mirror of
https://github.com/UnnoTed/wireguird
synced 2026-07-25 17:24:46 -04:00
new settings for country flags, reconnect, passwordless polkit policy
This commit is contained in:
parent
71c936a3b6
commit
1c61ed51c8
@ -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
|
||||
```
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
2
fileb0x.toml
Executable file → Normal 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
133
gui/countries/countries.go
Normal 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()
|
||||
}
|
||||
}
|
||||
63
gui/endpoints/endpoints.go
Normal file
63
gui/endpoints/endpoints.go
Normal 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
|
||||
}
|
||||
41
gui/endpoints/endpoints_test.go
Normal file
41
gui/endpoints/endpoints_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,7 @@ func main() {
|
||||
"Switch",
|
||||
"Window",
|
||||
"Entry",
|
||||
"Image",
|
||||
"Label",
|
||||
"Stack",
|
||||
"Box",
|
||||
|
||||
@ -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
87
gui/gui.go
Executable file → Normal 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
451
gui/tunnels.go
Executable file → Normal 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
42
main.go
Executable file → Normal 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)
|
||||
|
||||
@ -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
59
wireguird.glade
Executable file → Normal 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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user