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 {