Resolve merge conflicts for PR #72

This commit is contained in:
UnnoTed 2026-05-02 15:10:24 -03:00
parent ce395e50f2
commit 0c4c826cbe

493
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"
@ -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("<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 {