new settings for country flags, reconnect, passwordless polkit policy

This commit is contained in:
UnnoTed 2026-04-26 23:43:11 -03:00
parent 71c936a3b6
commit 1c61ed51c8
14 changed files with 776 additions and 155 deletions

View File

@ -34,13 +34,11 @@ sudo dpkg -i ./wireguird_amd64.deb
## Compile
deb dependencies: `wireguard-tools libgtk-3-dev libayatana-appindicator3-dev golang-go resolvconf`
deb dependencies: `sudo apt install wireguard-tools libgtk-3-dev libayatana-appindicator3-dev golang-go resolvconf`
```sh
git clone https://github.com/UnnoTed/wireguird
cd wireguird
chmod +x ./*.sh
./deps.sh
./package.sh
./install.sh
go install github.com/goreleaser/goreleaser/v2@latest
goreleaser release --snapshot --clean
```

View File

@ -1,3 +1,12 @@
wireguird (1.2) UNRELEASED; urgency=medium
* Add new setting to show country flags on tunnel names
* Change distribution packaging
* Update checks run once per run
* Add new setting to auto-reconnect on start
-- unknown <unknown@unknown> Sat, 26 Apr 2026 15:50:00 +0100
wireguird (1.1) UNRELEASED; urgency=medium
* Add ability to import multiple config files at once (zip archive)

View File

@ -1,6 +1,6 @@
Package: wireguird
Source: wireguird
Version: 1.1
Version: 1.2
Section: utils
Priority: extra
Architecture: amd64

2
fileb0x.toml Executable file → Normal file
View File

@ -116,7 +116,7 @@ debug = false
# type: array of objects
[[custom]]
# type: array of strings
files = ["*.glade"]
files = ["*.glade", "./Icon/", "./flags/"]
# base is the path that will be removed from all files' path
# type: string

133
gui/countries/countries.go Normal file
View File

@ -0,0 +1,133 @@
package countries
import (
"fmt"
"io"
"net"
"net/http"
"os"
"sync"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/rs/zerolog/log"
)
const (
mmdbURL = "https://github.com/iplocate/ip-address-databases/raw/refs/heads/main/ip-to-country/ip-to-country.mmdb"
mmdbFile = "./countries.mmdb"
)
var (
initErr error
once sync.Once // OpenDatabase runs once
db *maxminddb.Reader
ready = make(chan struct{})
dnsCache = map[string]string{}
dnsCacheMu sync.Mutex
)
type IPRecord struct {
CountryCode string `maxminddb:"country_code"`
Country struct {
IsoCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}
func OpenDatabase() error {
once.Do(func() {
defer close(ready)
log.Debug().Msg("initializing country database")
// downloads the db when doesnt exist or is older than a week
if s, err := os.Stat(mmdbFile); os.IsNotExist(err) || s.ModTime().Before(time.Now().Add(-7*24*time.Hour)) {
if err := downloadDatabase(); err != nil {
initErr = fmt.Errorf("failed to download country database: %w", err)
return
}
}
var err error
db, err = maxminddb.Open(mmdbFile)
if err != nil {
initErr = fmt.Errorf("failed to open country database: %w", err)
return
}
log.Debug().Msg("country database opened successfully")
})
return initErr
}
func downloadDatabase() error {
resp, err := http.Get(mmdbURL)
if err != nil {
return fmt.Errorf("failed to download mmdb: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
out, err := os.Create(mmdbFile)
if err != nil {
return fmt.Errorf("failed to create local file: %w", err)
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("failed to write data to disk: %w", err)
}
return nil
}
func Find(ipStr string) (string, error) {
<-ready
if initErr != nil {
return "", fmt.Errorf("database initialization failed: %w", initErr)
}
if val, ok := dnsCache[ipStr]; ok {
log.Debug().Str("ip", ipStr).Str("country", val).Msg("country found in cache")
return val, nil
}
ip := net.ParseIP(ipStr)
if ip == nil {
return "", fmt.Errorf("invalid IP address format: %s", ipStr)
}
var record IPRecord
err := db.Lookup(ip, &record)
if err != nil {
return "", fmt.Errorf("error during database lookup: %w", err)
}
if record.CountryCode != "" {
dnsCacheMu.Lock()
dnsCache[ipStr] = record.CountryCode
dnsCacheMu.Unlock()
return record.CountryCode, nil
}
if record.Country.IsoCode != "" {
dnsCacheMu.Lock()
dnsCache[ipStr] = record.Country.IsoCode
dnsCacheMu.Unlock()
return record.Country.IsoCode, nil
}
return "Unknown", nil
}
func CloseDatabase() {
if db != nil {
db.Close()
}
}

View File

@ -0,0 +1,63 @@
package endpoints
import (
"net"
"strings"
"unicode"
)
const (
Unknown = "Unknown"
IPV4 = "IPV4"
IPV6 = "IPV6"
FQDN = "FQDN"
)
func Categorize(endpoint string) string {
// no empty or urls
if endpoint == "" || strings.Contains(endpoint, "://") {
return Unknown
}
host, _, err := net.SplitHostPort(endpoint)
if err != nil {
// when no port is present, use the it as the host.
host = endpoint
}
// IPV6 with brackets
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
inner := host[1 : len(host)-1]
ip := net.ParseIP(inner)
if ip != nil && ip.To4() == nil {
return IPV6
}
// invalid [IPV6]
return Unknown
}
if ip := net.ParseIP(host); ip != nil {
if ip.To4() != nil {
return IPV4
}
return IPV6
}
// FQDNs have no web url chars like ?, #, @...
if host == "" || strings.ContainsAny(host, " /?#@:[]") {
return Unknown
}
// FQDNs must be printable and contain no spaces
// printable means that strange chars like chinese should work
for _, r := range host {
if !unicode.IsPrint(r) || unicode.IsSpace(r) {
return Unknown
}
}
return FQDN
}

View File

@ -0,0 +1,41 @@
package endpoints
import (
"testing"
)
func TestCategorization(t *testing.T) {
tests := []struct {
name string
endpoint string
want string
}{
{"Empty string", "", Unknown},
{"Just a colon", ":", Unknown},
{"Empty brackets", "[]", Unknown},
{"Empty brackets with port", "[]:51820", Unknown},
{"IPV4 with port", "192.168.1.1:51820", IPV4},
{"IPV4 without port", "10.0.0.1", IPV4},
{"IPV6 with brackets & port", "[2001:db8::1]:51820", IPV6},
{"IPV6 with brackets, no port", "[2001:db8::1]", IPV6},
{"IPV6 no brackets, no port", "2001:db8::1", IPV6},
{"Valid FQDN with port", "vpn.example.com:51820", FQDN},
{"Valid FQDN without port", "wg.my-domain.net", FQDN},
{"FQDN with spaces", "my vpn.com:51820", Unknown},
{"URL with scheme", "https://vpn.example.com", Unknown},
{"Malformed IP in brackets", "[192.168.1.1]", Unknown},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Categorize(tt.endpoint)
if got != tt.want {
t.Errorf("\nCategorize(%q)\n got: %s\n want: %s", tt.endpoint, got, tt.want)
}
})
}
}

View File

@ -27,6 +27,7 @@ func main() {
"Switch",
"Window",
"Entry",
"Image",
"Label",
"Stack",
"Box",

View File

@ -246,6 +246,20 @@ func Entry(name string) (*gtk.Entry, error) {
return entry1, nil
}
func Image(name string) (*gtk.Image, error) {
obj, err := Builder.GetObject(name)
if err != nil {
return nil, err
}
image1, ok := obj.(*gtk.Image)
if !ok {
return nil, errors.New("cant get *gtk.Image: " + name)
}
return image1, nil
}
func Label(name string) (*gtk.Label, error) {
obj, err := Builder.GetObject(name)
if err != nil {

87
gui/gui.go Executable file → Normal file
View File

@ -7,7 +7,10 @@ import (
"time"
"github.com/UnnoTed/go-appindicator"
"github.com/UnnoTed/wireguird/gui/countries"
"github.com/UnnoTed/wireguird/gui/get"
"github.com/UnnoTed/wireguird/static"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/rs/zerolog/log"
@ -15,14 +18,15 @@ import (
)
const (
Version = "1.1.0"
Version = "1.2.0"
Repo = "https://github.com/UnnoTed/wireguird"
TunnelsPath = "/etc/wireguard/"
IconPath = "/opt/wireguird/Icon/"
IconPath = "./Icon/"
)
var (
settingsWindow *gtk.Window
updateChecked bool
editorWindow *gtk.Window
application *gtk.Application
indicator *appindicator.Indicator
@ -99,7 +103,27 @@ func Create(app *gtk.Application, b *gtk.Builder, w *gtk.ApplicationWindow, ind
}()
}
if Settings.CountryFlags {
go func() {
if err := countries.OpenDatabase(); err != nil {
log.Error().Err(err).Msg("error getting country database")
}
}()
}
window.HideOnDelete()
icons := map[string]string{
"icon_delete": "Icon/delete.png",
"icon_tunnel": "Icon/add_tunnel.png",
"icon_zip": "Icon/zip.png",
}
for name, path := range icons {
if err := loadGlobalEmbeddedImage(name, path); err != nil {
return err
}
}
return nil
}
@ -167,6 +191,12 @@ func createSettings(show bool) (*gtk.Window, error) {
func updateCheck() error {
log.Info().Msg("Checking for updates")
// once per run
if updateChecked {
return nil
}
resp, err := http.Get("https://api.github.com/repos/UnnoTed/wireguird/releases")
if err != nil {
return err
@ -197,5 +227,58 @@ func updateCheck() error {
}
}
updateChecked = true
return nil
}
func loadEmbeddedImage(path string) (*gtk.Image, error) {
data, err := static.ReadFile(path)
if err != nil {
return nil, err
}
loader, err := gdk.PixbufLoaderNew()
if err != nil {
return nil, err
}
loader.Write(data)
loader.Close()
pixbuf, err := loader.GetPixbuf()
if err != nil {
return nil, err
}
log.Debug().Str("path", path).Msg("loaded embedded image")
return gtk.ImageNewFromPixbuf(pixbuf)
}
func loadGlobalEmbeddedImage(id string, path string) error {
data, err := static.ReadFile(path)
if err != nil {
return err
}
loader, err := gdk.PixbufLoaderNew()
if err != nil {
return err
}
loader.Write(data)
loader.Close()
pixbuf, err := loader.GetPixbuf()
if err != nil {
return err
}
img, err := get.Image(id)
if err != nil {
return err
}
img.SetFromPixbuf(pixbuf)
log.Debug().Str("path", path).Msg("loaded global embedded image")
return nil
}

451
gui/tunnels.go Executable file → Normal file
View File

@ -2,9 +2,12 @@ package gui
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
@ -17,6 +20,8 @@ import (
"github.com/ungerik/go-dry"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/UnnoTed/wireguird/gui/countries"
"github.com/UnnoTed/wireguird/gui/endpoints"
"github.com/UnnoTed/wireguird/gui/get"
"github.com/UnnoTed/wireguird/settings"
"github.com/dustin/go-humanize"
@ -56,16 +61,20 @@ type Tunnels struct {
}
Settings struct {
MultipleTunnels *gtk.CheckButton
StartOnTray *gtk.CheckButton
CheckUpdates *gtk.CheckButton
MultipleTunnels *gtk.CheckButton
StartOnTray *gtk.CheckButton
CheckUpdates *gtk.CheckButton
CountryFlags *gtk.CheckButton
RememberConnectedServers *gtk.CheckButton
Passwordless *gtk.CheckButton
}
ButtonChangeState *gtk.Button
icons map[string]*gtk.Image
ticker *time.Ticker
lastSelected string
lastSelected string
reconnectedRememberedServers bool
grayIcon *gtk.Image
greenIcon *gtk.Image
@ -76,12 +85,12 @@ func (t *Tunnels) Create() error {
t.ticker = time.NewTicker(1 * time.Second)
var err error
t.grayIcon, err = gtk.ImageNewFromFile(IconPath + "not_connected.png")
t.grayIcon, err = loadEmbeddedImage(IconPath + "not_connected.png")
if err != nil {
return err
}
t.greenIcon, err = gtk.ImageNewFromFile(IconPath + "connected.png")
t.greenIcon, err = loadEmbeddedImage(IconPath + "connected.png")
if err != nil {
return err
}
@ -207,122 +216,7 @@ func (t *Tunnels) Create() error {
}
t.ButtonChangeState.Connect("clicked", func() {
err := func() error {
list, err := wgc.Devices()
if err != nil {
return err
}
activeNames := t.ActiveDeviceName()
row := tl.GetSelectedRow()
// row not found for config
if row == nil {
return nil
}
// conf name
name, err := row.GetName()
if err != nil {
return err
}
// https://github.com/UnnoTed/wireguird/issues/11#issuecomment-1332047191
if len(name) >= 16 {
ShowError(window, errors.New("Tunnel's file name is too long ("+strconv.Itoa(len(name))+"), max length: 15"))
}
// disconnect from given tunnel
dc := func(d *wgtypes.Device) error {
glib.IdleAdd(func() {
t.icons[d.Name].SetFromPixbuf(t.grayIcon.GetPixbuf())
})
c := exec.Command("wg-quick", "down", d.Name)
output, err := c.Output()
if err != nil {
es := string(err.(*exec.ExitError).Stderr)
log.Error().Err(err).Str("output", string(output)).Str("error", es).Msg("wg-quick down error")
oerr := err.Error() + "\nwg-quick's output:\n" + es
wlog("ERROR", oerr)
return errors.New(oerr)
}
indicator.SetIcon("wireguard_off")
return wlog("INFO", "Disconnected from "+d.Name)
}
// disconnects from all tunnels before connecting to a new one
// when the multipleTunnels option is disabled
if Settings.MultipleTunnels {
log.Info().Str("name", name).Msg("NAME")
d, err := wgc.Device(name)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if !errors.Is(err, os.ErrNotExist) {
if err := dc(d); err != nil {
return err
}
}
} else {
for _, d := range list {
if err := dc(d); err != nil {
return err
}
}
}
// dont connect to the new one as this is a disconnect action
if dry.StringListContains(activeNames, name) {
t.UpdateRow(row)
glib.IdleAdd(func() {
if len(activeNames) == 1 {
header.SetSubtitle("Not connected!")
} else {
activeNames := t.ActiveDeviceName()
header.SetSubtitle("Connected to " + strings.Join(activeNames, ", "))
}
})
return nil
}
// connect to a tunnel
c := exec.Command("wg-quick", "up", name)
output, err := c.Output()
if err != nil {
es := string(err.(*exec.ExitError).Stderr)
log.Error().Err(err).Str("output", string(output)).Str("error", es).Msg("wg-quick up error")
oerr := err.Error() + "\nwg-quick's output:\n" + es
wlog("ERROR", oerr)
return errors.New(oerr)
}
// update header label with tunnel names
glib.IdleAdd(func() {
activeNames := t.ActiveDeviceName()
header.SetSubtitle("Connected to " + strings.Join(activeNames, ", "))
})
// set icon to connected for the tunnel's row
glib.IdleAdd(func() {
t.icons[name].SetFromPixbuf(t.greenIcon.GetPixbuf())
t.UpdateRow(row)
indicator.SetIcon("wg_connected")
})
if err := wlog("INFO", "Connected to "+name); err != nil {
return err
}
return nil
}()
if err != nil {
if err = t.Connect(tl.GetSelectedRow(), ""); err != nil {
ShowError(window, err)
}
})
@ -793,6 +687,8 @@ func (t *Tunnels) Create() error {
}
btnSettingsSave.Connect("clicked", func() {
prevCountryFlags := Settings.CountryFlags
err := func() error {
t.ToSettings()
Settings.Save()
@ -804,6 +700,24 @@ func (t *Tunnels) Create() error {
if err != nil {
ShowError(window, err, "settings save error")
}
// only download country database when previously disabled and now enabled
if !prevCountryFlags && Settings.CountryFlags {
go func() {
err := countries.OpenDatabase()
if err != nil {
ShowError(window, err, "download country database error")
}
}()
}
if err := t.SetPasswordless(); err != nil {
ShowError(window, err, "set passwordless error")
}
if err := t.ScanTunnels(); err != nil {
ShowError(window, err, "scan tunnels error")
}
})
// button: settings cancel
@ -908,6 +822,18 @@ func (t *Tunnels) Create() error {
}
}()
go func() {
if Settings.RememberConnectedServers && Settings.ConnectedServers != nil && len(Settings.ConnectedServers) > 0 {
for tunnel := range Settings.ConnectedServers {
if err := t.Connect(nil, tunnel); err != nil {
log.Error().Err(err).Str("tunnel", tunnel).Msg("error connecting to tunnel")
wlog("ERROR", "error connecting to tunnel")
}
}
}
t.reconnectedRememberedServers = true
}()
return nil
}
@ -915,6 +841,9 @@ func (t *Tunnels) ToSettings() {
Settings.MultipleTunnels = t.Settings.MultipleTunnels.GetActive()
Settings.StartOnTray = t.Settings.StartOnTray.GetActive()
Settings.CheckUpdates = t.Settings.CheckUpdates.GetActive()
Settings.CountryFlags = t.Settings.CountryFlags.GetActive()
Settings.RememberConnectedServers = t.Settings.RememberConnectedServers.GetActive()
Settings.Passwordless = t.Settings.Passwordless.GetActive()
}
func (t *Tunnels) FromSettings() error {
@ -941,6 +870,27 @@ func (t *Tunnels) FromSettings() error {
}
t.Settings.CheckUpdates.SetActive(Settings.CheckUpdates)
// checkbox: country flags
t.Settings.CountryFlags, err = get.CheckButton("settings_country_flags")
if err != nil {
return err
}
t.Settings.CountryFlags.SetActive(Settings.CountryFlags)
// checkbox: remember connected servers
t.Settings.RememberConnectedServers, err = get.CheckButton("settings_remember_connected")
if err != nil {
return err
}
t.Settings.RememberConnectedServers.SetActive(Settings.RememberConnectedServers)
// checkbox: passwordless
t.Settings.Passwordless, err = get.CheckButton("settings_polkit_policy")
if err != nil {
return err
}
t.Settings.Passwordless.SetActive(Settings.Passwordless)
return nil
}
@ -1028,13 +978,17 @@ func (t *Tunnels) ScanTunnels() error {
// showError(err)
return err
}
configByName := map[string]string{}
for _, fileName := range list {
if !strings.HasSuffix(fileName, ".conf") {
continue
}
configList = append(configList, strings.TrimSuffix(fileName, ".conf"))
name := strings.TrimSuffix(fileName, ".conf")
configByName[name] = TunnelsPath + fileName
configList = append(configList, name)
}
tl, err := get.ListBox("tunnel_list")
@ -1056,7 +1010,11 @@ func (t *Tunnels) ScanTunnels() error {
})
activeNames := t.ActiveDeviceName()
header.SetSubtitle("Connected to " + strings.Join(activeNames, ", "))
if len(activeNames) > 0 {
header.SetSubtitle("Connected to " + strings.Join(activeNames, ", "))
} else {
header.SetSubtitle("No active tunnels")
}
lasti := len(configList) - 1
for i, name := range configList {
@ -1097,6 +1055,82 @@ func (t *Tunnels) ScanTunnels() error {
img.SetVExpand(false)
img.SetHExpand(false)
var country *gtk.Image
if Settings.CountryFlags {
cfg, err := ini.Load(configByName[name])
if err != nil {
return err
}
peersec := cfg.Section("Peer")
nothing, err := loadEmbeddedImage("./flags/IDK.png")
if err != nil {
return err
}
country = nothing
country.SetVAlign(gtk.ALIGN_CENTER)
country.SetHAlign(gtk.ALIGN_START)
country.SetSizeRequest(10, 10)
country.SetVExpand(false)
country.SetHExpand(false)
go func() {
err := func() error {
endpoint := strings.Split(peersec.Key("Endpoint").String(), ":")[0]
log.Debug().Str("endpoint", endpoint).Msg("checking country flag for tunnel")
ip := ""
switch endpoints.Categorize(endpoint) {
case endpoints.IPV4, endpoints.IPV6:
ip = endpoint
case endpoints.FQDN:
ips, err := net.LookupIP(endpoint)
if err != nil {
log.Error().Err(err).Str("endpoint", endpoint).Msg("dns lookup error")
return err
}
if len(ips) > 0 {
ip = ips[0].String()
} else {
return errors.New("endpoint \"" + endpoint + "\" coudln't find ip from dns")
}
default:
return errors.New("endpoint \"" + endpoint + "\" is invalid")
}
log.Debug().Str("endpoint", endpoint).Str("ip", ip).Msg("found ip for tunnel endpoint")
countryCode, err := countries.Find(ip)
if err != nil {
log.Error().Err(err).Msg("find country error")
return err
}
log.Debug().Str("country", countryCode).Str("ip", ip).Msg("found country for tunnel")
flag, err := loadEmbeddedImage("./flags/" + countryCode + ".png")
if err != nil {
log.Error().Err(err).Str("country", countryCode).Str("ip", ip).Msg("error loading embedded image")
return err
}
// prevents crash when deleting servers
glib.IdleAdd(func() {
if country != nil && country.GetPixbuf() != nil {
country.SetFromPixbuf(flag.GetPixbuf())
}
})
return nil
}()
if err != nil {
log.Error().Err(err).Msg("country flag error")
wlog("ERROR", "country flag error: "+err.Error())
}
}()
}
label, err := gtk.LabelNew(name)
if err != nil {
return err
@ -1114,6 +1148,11 @@ func (t *Tunnels) ScanTunnels() error {
}
box.Add(img)
if Settings.CountryFlags {
box.Add(country)
}
box.Add(label)
row.SetName(name)
@ -1171,6 +1210,170 @@ func (t *Tunnels) ActiveDeviceName() []string {
return names
}
func (t *Tunnels) Connect(row *gtk.ListBoxRow, name string) error {
list, err := wgc.Devices()
if err != nil {
return err
}
activeNames := t.ActiveDeviceName()
if row != nil && name == "" {
// conf name
name, err = row.GetName()
if err != nil {
return err
}
}
// https://github.com/UnnoTed/wireguird/issues/11#issuecomment-1332047191
if len(name) >= 16 {
ShowError(window, errors.New("Tunnel's file name is too long ("+strconv.Itoa(len(name))+"), max length: 15"))
}
// disconnect from given tunnel
dc := func(d *wgtypes.Device) error {
glib.IdleAdd(func() {
t.icons[d.Name].SetFromPixbuf(t.grayIcon.GetPixbuf())
})
c := exec.Command("wg-quick", "down", d.Name)
output, err := c.Output()
if err != nil {
es := string(err.(*exec.ExitError).Stderr)
log.Error().Err(err).Str("output", string(output)).Str("error", es).Msg("wg-quick down error")
oerr := err.Error() + "\nwg-quick's output:\n" + es
wlog("ERROR", oerr)
return errors.New(oerr)
}
if Settings.RememberConnectedServers && Settings.ConnectedServers != nil && len(Settings.ConnectedServers) > 0 && t.reconnectedRememberedServers {
delete(Settings.ConnectedServers, d.Name)
if err := Settings.Save(); err != nil {
log.Error().Err(err).Msg("Error saving settings on disconnect")
wlog("INFO", "Error saving settings on disconnect: "+err.Error())
}
}
indicator.SetIcon("wireguard_off")
return wlog("INFO", "Disconnected from "+d.Name)
}
// disconnects from all tunnels before connecting to a new one
// when the multipleTunnels option is disabled
if Settings.MultipleTunnels {
log.Info().Str("name", name).Msg("NAME")
d, err := wgc.Device(name)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if !errors.Is(err, os.ErrNotExist) {
if err := dc(d); err != nil {
return err
}
}
} else {
for _, d := range list {
if err := dc(d); err != nil {
return err
}
}
}
// dont connect to the new one as this is a disconnect action
if dry.StringListContains(activeNames, name) {
t.UpdateRow(row)
glib.IdleAdd(func() {
if len(activeNames) == 1 {
header.SetSubtitle("Not connected!")
} else {
activeNames := t.ActiveDeviceName()
header.SetSubtitle("Connected to " + strings.Join(activeNames, ", "))
}
})
return nil
}
// connect to a tunnel
c := exec.Command("wg-quick", "up", name)
output, err := c.Output()
if err != nil {
es := string(err.(*exec.ExitError).Stderr)
log.Error().Err(err).Str("output", string(output)).Str("error", es).Msg("wg-quick up error")
oerr := err.Error() + "\nwg-quick's output:\n" + es
wlog("ERROR", oerr)
return errors.New(oerr)
}
if Settings.RememberConnectedServers {
// using time because order may be important
if Settings.ConnectedServers == nil {
Settings.ConnectedServers = map[string]int64{}
}
Settings.ConnectedServers[name] = time.Now().Unix()
if err := Settings.Save(); err != nil {
log.Error().Err(err).Msg("Error saving settings on connect")
}
}
// update header label with tunnel names
glib.IdleAdd(func() {
activeNames := t.ActiveDeviceName()
header.SetSubtitle("Connected to " + strings.Join(activeNames, ", "))
})
// set icon to connected for the tunnel's row
glib.IdleAdd(func() {
t.icons[name].SetFromPixbuf(t.greenIcon.GetPixbuf())
t.UpdateRow(row)
indicator.SetIcon("wg_connected")
})
if err := wlog("INFO", "Connected to "+name); err != nil {
return err
}
return nil
}
func (t *Tunnels) SetPasswordless() error {
// not installed
if !dry.FileExists("/usr/share/polkit-1/actions/wireguird.policy") {
return nil
}
content, err := ioutil.ReadFile("/usr/share/polkit-1/actions/wireguird.policy")
if err != nil {
return err
}
changed := false
if Settings.Passwordless && bytes.Contains(content, []byte("<allow_any>auth_admin</allow_any>")) {
content = bytes.Replace(content, []byte("<allow_any>auth_admin</allow_any>"), []byte("<allow_any>yes</allow_any>"), 1)
content = bytes.Replace(content, []byte("<allow_inactive>auth_admin</allow_inactive>"), []byte("<allow_inactive>yes</allow_inactive>"), 1)
content = bytes.Replace(content, []byte("<allow_active>auth_admin_keep</allow_active>"), []byte("<allow_active>yes</allow_active>"), 1)
changed = true
} else if !Settings.Passwordless && bytes.Contains(content, []byte("<allow_any>yes</allow_any>")) {
content = bytes.Replace(content, []byte("<allow_any>yes</allow_any>"), []byte("<allow_any>auth_admin</allow_any>"), 1)
content = bytes.Replace(content, []byte("<allow_inactive>yes</allow_inactive>"), []byte("<allow_inactive>auth_admin</allow_inactive>"), 1)
content = bytes.Replace(content, []byte("<allow_active>yes</allow_active>"), []byte("<allow_active>auth_admin_keep</allow_active>"), 1)
changed = true
}
if changed {
if err := ioutil.WriteFile("/usr/share/polkit-1/actions/wireguird.policy", content, 0644); err != nil {
return err
}
}
return nil
}
func wlog(t string, text string) error {
wlogs, err := get.ListBox("wireguard_logs")
if err != nil {

42
main.go Executable file → Normal file
View File

@ -3,11 +3,14 @@
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/UnnoTed/go-appindicator"
"github.com/UnnoTed/horizontal"
"github.com/UnnoTed/wireguird/gui"
"github.com/UnnoTed/wireguird/gui/countries"
"github.com/UnnoTed/wireguird/static"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
@ -15,9 +18,31 @@ import (
"github.com/rs/zerolog/log"
)
var iconTmpDir = os.TempDir()
var win *gtk.ApplicationWindow
func main() {
if os.Geteuid() != 0 {
fmt.Println("\n\n\n\n")
fmt.Println("===========================================================================")
fmt.Println("===========================================================================")
fmt.Println("===========================================================================")
fmt.Println("=== ===")
fmt.Println("=== ===")
fmt.Println("=== Wireguird must be ran as root to access /etc/wireguard and wg-quick ===")
fmt.Println("=== Use pkexec to run as root: ===")
fmt.Println("=== pkexec wireguird ===")
fmt.Println("=== ===")
fmt.Println("=== ===")
fmt.Println("===========================================================================")
fmt.Println("===========================================================================")
fmt.Println("===========================================================================")
return
}
defer os.Remove(iconTmpDir)
defer countries.CloseDatabase()
log.Logger = log.Output(horizontal.ConsoleWriter{Out: os.Stderr})
log.Info().Uint("major", gtk.GetMajorVersion()).Uint("minor", gtk.GetMinorVersion()).Uint("micro", gtk.GetMicroVersion()).Msg("GTK Version")
@ -57,8 +82,23 @@ func createTray(application *gtk.Application) (*appindicator.Indicator, error) {
return nil, err
}
icons := []string{"wireguard_off", "wg_connected"}
for _, icon := range icons {
iconBytes, err := static.ReadFile("./Icon/" + icon + ".png")
if err != nil {
log.Error().Err(err).Str("icon", icon).Msg("Error reading embedded icon")
return nil, err
}
iconPath := filepath.Join(iconTmpDir, icon+".png")
if err := os.WriteFile(iconPath, iconBytes, 0644); err != nil {
log.Error().Err(err).Str("icon", icon).Msg("Failed to write temp icon")
return nil, err
}
}
indicator := appindicator.New(application.GetApplicationID(), "wireguard_off", appindicator.CategoryApplicationStatus)
indicator.SetIconThemePath("/opt/wireguird/Icon")
indicator.SetIconThemePath(iconTmpDir)
indicator.SetTitle("Wireguird")
// indicator.SetLabel("Wireguird", "")
indicator.SetStatus(appindicator.StatusActive)

View File

@ -12,19 +12,16 @@ import (
const FilePath = "./wireguird.settings"
type Settings struct {
MultipleTunnels bool
StartOnTray bool
CheckUpdates bool
Debug bool
MultipleTunnels bool
StartOnTray bool
CheckUpdates bool
Debug bool
RememberConnectedServers bool
ConnectedServers map[string]int64
CountryFlags bool
Passwordless bool
}
var (
multipleTunnels *bool
checkUpdates *bool
debug *bool
tray *bool
)
func (s *Settings) Init() error {
log.Debug().Msg("Settings init")

59
wireguird.glade Executable file → Normal file
View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkWindow" id="editor_window">
@ -156,21 +156,18 @@
<object class="GtkImage" id="icon_delete">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixbuf">/opt/wireguird/Icon/delete.png</property>
</object>
<object class="GtkImage" id="icon_tunnel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixbuf">/opt/wireguird/Icon/add_tunnel.png</property>
</object>
<object class="GtkImage" id="icon_zip">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixbuf">/opt/wireguird/Icon/zip.png</property>
</object>
<object class="GtkApplicationWindow" id="main_window">
<property name="width-request">650</property>
<property name="height-request">470</property>
<property name="width-request">720</property>
<property name="height-request">480</property>
<property name="can-focus">False</property>
<property name="window-position">center</property>
<property name="show-menubar">False</property>
@ -519,7 +516,7 @@
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Interface:</property>
<property name="label" translatable="yes"> Interface</property>
</object>
</child>
</object>
@ -715,7 +712,7 @@
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Peer</property>
<property name="label" translatable="yes"> Peer</property>
</object>
</child>
</object>
@ -892,7 +889,7 @@
</child>
<child>
<object class="GtkCheckButton" id="settings_start_tray">
<property name="label" translatable="yes">Start in system tray</property>
<property name="label" translatable="yes">Launch minimized to system tray</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
@ -918,6 +915,20 @@
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="settings_remember_connected">
<property name="label" translatable="yes">Auto-reconnect to previous servers on launch</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
@ -965,7 +976,35 @@
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="settings_country_flags">
<property name="label" translatable="yes">Country flags (local db 16MB, updated weekly via github.com/iplocate/ip-address-databases)</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="settings_polkit_policy">
<property name="label" translatable="yes">Enable passwordless authentication (less secure)</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>