From 654eff94a4e3db674c3b5ad40ba3dd22faccf11f Mon Sep 17 00:00:00 2001 From: Nikolay Lazarov Date: Thu, 31 Oct 2024 02:28:11 +0200 Subject: [PATCH 1/3] Refactor button change state logic This commit extracts the following functions from the button change state callback: - `IsTunnelConnected(name string) (bool, error)` - checks if the tunnels is connected or returns an error - `DisconnectTunnel(name string) error` - disconnects from the tunnel or returns an error - `ConnectTunnel(name string) error` - connects to the tunnel or returns an error - `ToggleTunnel(name string) error` - toggles the connected/disconnected status of the tunnel --- gui/tunnels.go | 198 ++++++++++++++++++++++++------------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/gui/tunnels.go b/gui/tunnels.go index 8906335..de8aae6 100755 --- a/gui/tunnels.go +++ b/gui/tunnels.go @@ -14,9 +14,6 @@ import ( "strings" "time" - "github.com/ungerik/go-dry" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "github.com/UnnoTed/wireguird/gui/get" "github.com/UnnoTed/wireguird/settings" "github.com/dustin/go-humanize" @@ -24,6 +21,7 @@ import ( "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "github.com/rs/zerolog/log" + "github.com/ungerik/go-dry" "gopkg.in/ini.v1" ) @@ -208,12 +206,6 @@ 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 { @@ -226,99 +218,11 @@ func (t *Tunnels) Create() error { 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 { + if err := t.ToggleTunnel(name); err != nil { return err } + t.UpdateRow(row) return nil }() @@ -911,6 +815,102 @@ func (t *Tunnels) Create() error { return nil } +func (t *Tunnels) ToggleTunnel(name string) error { + if len(name) >= 16 { + ShowError(window, errors.New("Tunnel's file name is too long ("+strconv.Itoa(len(name))+"), max length: 15")) + } + + defer func() { + activeNames := t.ActiveDeviceName() + if len(activeNames) == 0 { + indicator.SetIcon("wireguard_off") + glib.IdleAdd(func() { + header.SetSubtitle("Not connected!") + }) + } else { + indicator.SetIcon("wg_connected") + glib.IdleAdd(func() { + header.SetSubtitle("Connected to " + strings.Join(activeNames, ", ")) + }) + } + }() + + isConnected, err := t.IsTunnelConnected(name) + if err != nil { + return err + } + + if Settings.MultipleTunnels { + if isConnected { + return t.DisconnectTunnel(name) + } else { + return t.ConnectTunnel(name) + } + } else { + // disconnect from all active tunnels + devices, err := wgc.Devices() + if err != nil { + return err + } + for _, d := range devices { + if err := t.DisconnectTunnel(d.Name); err != nil { + return err + } + } + + if !isConnected { + return t.ConnectTunnel(name) + } + return nil + } +} + +func (t *Tunnels) DisconnectTunnel(name string) error { + c := exec.Command("wg-quick", "down", 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) + } + + glib.IdleAdd(func() { + t.icons[name].SetFromPixbuf(t.grayIcon.GetPixbuf()) + }) + + return wlog("INFO", "Disconnected from "+name) +} + +func (t *Tunnels) ConnectTunnel(name string) error { + 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) + } + + glib.IdleAdd(func() { + t.icons[name].SetFromPixbuf(t.greenIcon.GetPixbuf()) + }) + + return wlog("INFO", "Connected to "+name) +} + +func (t *Tunnels) IsTunnelConnected(name string) (bool, error) { + d, err := wgc.Device(name) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return false, err + } + return err == nil && d != nil, nil +} + func (t *Tunnels) ToSettings() { Settings.MultipleTunnels = t.Settings.MultipleTunnels.GetActive() Settings.StartOnTray = t.Settings.StartOnTray.GetActive() From ce395e50f2ca3cf1a2bd92870b7c11623129dfc1 Mon Sep 17 00:00:00 2001 From: Nikolay Lazarov Date: Thu, 31 Oct 2024 02:31:49 +0200 Subject: [PATCH 2/3] Add tray submenu for VPN tunnels This commit adds a menu item in the tray labeled "Tunnels", which shows a submenu with available VPN tunnels and a checkbox indicating if the tunnel is connected or disconnected. Clicking on a tunnel toggles its connected/disconnected status. --- gui/tunnels.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/gui/tunnels.go b/gui/tunnels.go index de8aae6..0cc87a7 100755 --- a/gui/tunnels.go +++ b/gui/tunnels.go @@ -67,13 +67,27 @@ type Tunnels struct { grayIcon *gtk.Image greenIcon *gtk.Image + + menuTunnels *gtk.MenuItem + tunnelsTrayMenuItems map[string]*gtk.CheckMenuItem + tunnelsTrayMenuHandles map[string]glib.SignalHandle } func (t *Tunnels) Create() error { - t.icons = map[string]*gtk.Image{} + t.icons = make(map[string]*gtk.Image) + t.tunnelsTrayMenuItems = make(map[string]*gtk.CheckMenuItem) + t.tunnelsTrayMenuHandles = make(map[string]glib.SignalHandle) t.ticker = time.NewTicker(1 * time.Second) var err error + menu := indicator.GetMenu() + t.menuTunnels, err = gtk.MenuItemNewWithLabel("Tunnels") + if err != nil { + return err + } + menu.Add(t.menuTunnels) + menu.ReorderChild(t.menuTunnels, 1) + t.grayIcon, err = gtk.ImageNewFromFile(IconPath + "not_connected.png") if err != nil { return err @@ -879,6 +893,10 @@ func (t *Tunnels) DisconnectTunnel(name string) error { glib.IdleAdd(func() { t.icons[name].SetFromPixbuf(t.grayIcon.GetPixbuf()) + menuItem := t.tunnelsTrayMenuItems[name] + menuItem.HandlerBlock(t.tunnelsTrayMenuHandles[name]) + menuItem.SetActive(false) + menuItem.HandlerUnblock(t.tunnelsTrayMenuHandles[name]) }) return wlog("INFO", "Disconnected from "+name) @@ -898,6 +916,10 @@ func (t *Tunnels) ConnectTunnel(name string) error { glib.IdleAdd(func() { t.icons[name].SetFromPixbuf(t.greenIcon.GetPixbuf()) + menuItem := t.tunnelsTrayMenuItems[name] + menuItem.HandlerBlock(t.tunnelsTrayMenuHandles[name]) + menuItem.SetActive(true) + menuItem.HandlerUnblock(t.tunnelsTrayMenuHandles[name]) }) return wlog("INFO", "Connected to "+name) @@ -1058,6 +1080,11 @@ func (t *Tunnels) ScanTunnels() error { activeNames := t.ActiveDeviceName() header.SetSubtitle("Connected to " + strings.Join(activeNames, ", ")) + tunnelsMenu, err := gtk.MenuNew() + if err != nil { + return err + } + lasti := len(configList) - 1 for i, name := range configList { row, err := gtk.ListBoxRowNew() @@ -1073,6 +1100,18 @@ func (t *Tunnels) ScanTunnels() error { row.SetMarginBottom(8) } + menuItem, err := gtk.CheckMenuItemNewWithLabel(name) + if err != nil { + return err + } + tunnelsMenu.Add(menuItem) + tunnelName := name // this new variable is necessary - https://go.dev/blog/loopvar-preview + handle := menuItem.Connect("toggled", func() { + t.ToggleTunnel(tunnelName) + }) + t.tunnelsTrayMenuItems[name] = menuItem + t.tunnelsTrayMenuHandles[name] = handle + var img *gtk.Image if dry.StringListContains(activeNames, name) { @@ -1081,12 +1120,18 @@ func (t *Tunnels) ScanTunnels() error { return err } img = green + menuItem.HandlerBlock(handle) + menuItem.SetActive(true) + menuItem.HandlerUnblock(handle) } else { gray, err := gtk.ImageNewFromPixbuf(t.grayIcon.GetPixbuf()) if err != nil { return err } img = gray + menuItem.HandlerBlock(handle) + menuItem.SetActive(false) + menuItem.HandlerUnblock(handle) } t.icons[name] = img @@ -1135,6 +1180,9 @@ func (t *Tunnels) ScanTunnels() error { } } + t.menuTunnels.SetSubmenu(tunnelsMenu) + indicator.GetMenu().ShowAll() + return nil } From 0c4c826cbe224985e5137451848aa432fe24cd98 Mon Sep 17 00:00:00 2001 From: UnnoTed <9157736+UnnoTed@users.noreply.github.com> Date: Sat, 2 May 2026 15:10:24 -0300 Subject: [PATCH 3/3] Resolve merge conflicts for PR #72 --- gui/tunnels.go | 493 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 358 insertions(+), 135 deletions(-) mode change 100755 => 100644 gui/tunnels.go diff --git a/gui/tunnels.go b/gui/tunnels.go old mode 100755 new mode 100644 index 0cc87a7..9881aea --- a/gui/tunnels.go +++ b/gui/tunnels.go @@ -2,9 +2,12 @@ package gui import ( "archive/zip" + "bytes" "errors" "fmt" "io" + "io/ioutil" + "net" "os" "os/exec" "path/filepath" @@ -14,6 +17,11 @@ import ( "strings" "time" + "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" @@ -21,7 +29,6 @@ import ( "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "github.com/rs/zerolog/log" - "github.com/ungerik/go-dry" "gopkg.in/ini.v1" ) @@ -54,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 @@ -74,7 +85,7 @@ type Tunnels struct { } func (t *Tunnels) Create() error { - t.icons = make(map[string]*gtk.Image) + t.icons = map[string]*gtk.Image{} t.tunnelsTrayMenuItems = make(map[string]*gtk.CheckMenuItem) t.tunnelsTrayMenuHandles = make(map[string]glib.SignalHandle) t.ticker = time.NewTicker(1 * time.Second) @@ -88,12 +99,12 @@ func (t *Tunnels) Create() error { menu.Add(t.menuTunnels) menu.ReorderChild(t.menuTunnels, 1) - 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 } @@ -219,28 +230,7 @@ func (t *Tunnels) Create() error { } t.ButtonChangeState.Connect("clicked", func() { - err := func() error { - row := tl.GetSelectedRow() - // row not found for config - if row == nil { - return nil - } - - // conf name - name, err := row.GetName() - if err != nil { - return err - } - - if err := t.ToggleTunnel(name); err != nil { - return err - } - - t.UpdateRow(row) - return nil - }() - - if err != nil { + if err = t.ToggleTunnel(tl.GetSelectedRow(), ""); err != nil { ShowError(window, err) } }) @@ -711,6 +701,8 @@ func (t *Tunnels) Create() error { } btnSettingsSave.Connect("clicked", func() { + prevCountryFlags := Settings.CountryFlags + err := func() error { t.ToSettings() Settings.Save() @@ -722,6 +714,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 @@ -826,117 +836,28 @@ func (t *Tunnels) Create() error { } }() - return nil -} - -func (t *Tunnels) ToggleTunnel(name string) error { - if len(name) >= 16 { - ShowError(window, errors.New("Tunnel's file name is too long ("+strconv.Itoa(len(name))+"), max length: 15")) - } - - defer func() { - activeNames := t.ActiveDeviceName() - if len(activeNames) == 0 { - indicator.SetIcon("wireguard_off") - glib.IdleAdd(func() { - header.SetSubtitle("Not connected!") - }) - } else { - indicator.SetIcon("wg_connected") - glib.IdleAdd(func() { - header.SetSubtitle("Connected to " + strings.Join(activeNames, ", ")) - }) - } - }() - - isConnected, err := t.IsTunnelConnected(name) - if err != nil { - return err - } - - if Settings.MultipleTunnels { - if isConnected { - return t.DisconnectTunnel(name) - } else { - return t.ConnectTunnel(name) - } - } else { - // disconnect from all active tunnels - devices, err := wgc.Devices() - if err != nil { - return err - } - for _, d := range devices { - if err := t.DisconnectTunnel(d.Name); err != nil { - return err + go func() { + if Settings.RememberConnectedServers && Settings.ConnectedServers != nil && len(Settings.ConnectedServers) > 0 { + for tunnel := range Settings.ConnectedServers { + if err := t.ToggleTunnel(nil, tunnel); err != nil { + log.Error().Err(err).Str("tunnel", tunnel).Msg("error connecting to tunnel") + wlog("ERROR", "error connecting to tunnel") + } } } - if !isConnected { - return t.ConnectTunnel(name) - } - return nil - } -} - -func (t *Tunnels) DisconnectTunnel(name string) error { - c := exec.Command("wg-quick", "down", 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) - } - - glib.IdleAdd(func() { - t.icons[name].SetFromPixbuf(t.grayIcon.GetPixbuf()) - menuItem := t.tunnelsTrayMenuItems[name] - menuItem.HandlerBlock(t.tunnelsTrayMenuHandles[name]) - menuItem.SetActive(false) - menuItem.HandlerUnblock(t.tunnelsTrayMenuHandles[name]) - }) - - return wlog("INFO", "Disconnected from "+name) -} - -func (t *Tunnels) ConnectTunnel(name string) error { - 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) - } - - glib.IdleAdd(func() { - t.icons[name].SetFromPixbuf(t.greenIcon.GetPixbuf()) - menuItem := t.tunnelsTrayMenuItems[name] - menuItem.HandlerBlock(t.tunnelsTrayMenuHandles[name]) - menuItem.SetActive(true) - menuItem.HandlerUnblock(t.tunnelsTrayMenuHandles[name]) - }) - - return wlog("INFO", "Connected to "+name) -} - -func (t *Tunnels) IsTunnelConnected(name string) (bool, error) { - d, err := wgc.Device(name) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return false, err - } - return err == nil && d != nil, nil + t.reconnectedRememberedServers = true + }() + return nil } 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 { @@ -963,6 +884,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 } @@ -1050,13 +992,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") @@ -1078,7 +1024,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") + } tunnelsMenu, err := gtk.MenuNew() if err != nil { @@ -1107,7 +1057,10 @@ func (t *Tunnels) ScanTunnels() error { tunnelsMenu.Add(menuItem) tunnelName := name // this new variable is necessary - https://go.dev/blog/loopvar-preview handle := menuItem.Connect("toggled", func() { - t.ToggleTunnel(tunnelName) + err := t.ToggleTunnel(nil, tunnelName) + if err != nil { + wlog("ERROR", "connect tunnel error: "+err.Error()) + } }) t.tunnelsTrayMenuItems[name] = menuItem t.tunnelsTrayMenuHandles[name] = handle @@ -1142,6 +1095,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 @@ -1159,6 +1188,11 @@ func (t *Tunnels) ScanTunnels() error { } box.Add(img) + + if Settings.CountryFlags { + box.Add(country) + } + box.Add(label) row.SetName(name) @@ -1219,6 +1253,195 @@ func (t *Tunnels) ActiveDeviceName() []string { return names } +func (t *Tunnels) ToggleTunnel(row *gtk.ListBoxRow, name string) error { + list, err := wgc.Devices() + if err != nil { + return err + } + + if row == nil && name != "" { + if tl, err := get.ListBox("tunnel_list"); err == nil { + for i := uint(0); ; i++ { + r := tl.GetRowAtIndex(int(i)) + if r == nil { + break + } + if rname, _ := r.GetName(); rname == name { + row = r + break + } + } + } + } + + 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()) + if menuItem, ok := t.tunnelsTrayMenuItems[d.Name]; ok { + menuItem.HandlerBlock(t.tunnelsTrayMenuHandles[d.Name]) + menuItem.SetActive(false) + menuItem.HandlerUnblock(t.tunnelsTrayMenuHandles[d.Name]) + } + }) + + 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 menuItem, ok := t.tunnelsTrayMenuItems[name]; ok { + menuItem.HandlerBlock(t.tunnelsTrayMenuHandles[name]) + menuItem.SetActive(true) + menuItem.HandlerUnblock(t.tunnelsTrayMenuHandles[name]) + } + }) + + 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("auth_admin")) { + content = bytes.Replace(content, []byte("auth_admin"), []byte("yes"), 1) + content = bytes.Replace(content, []byte("auth_admin"), []byte("yes"), 1) + content = bytes.Replace(content, []byte("auth_admin_keep"), []byte("yes"), 1) + changed = true + } else if !Settings.Passwordless && bytes.Contains(content, []byte("yes")) { + content = bytes.Replace(content, []byte("yes"), []byte("auth_admin"), 1) + content = bytes.Replace(content, []byte("yes"), []byte("auth_admin"), 1) + content = bytes.Replace(content, []byte("yes"), []byte("auth_admin_keep"), 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 {