adapt new implementation

This commit is contained in:
Mark Puha
2023-09-19 18:03:01 +02:00
parent 15d899ff49
commit 1d06dcf3a6
29 changed files with 109 additions and 4896 deletions

View File

@@ -9,28 +9,30 @@ import (
"bytes"
"log"
"net"
"net/netip"
"sort"
"github.com/amnezia-vpn/amnezia-wg/tun"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/tun"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/tunnel/firewall"
"github.com/amnezia-vpn/awg-windows/tunnel/winipcfg"
)
func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []net.IPNet) {
func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []netip.Prefix) {
if len(addresses) == 0 {
return
}
includedInAddresses := func(a net.IPNet) bool {
// TODO: this makes the whole algorithm O(n^2). But we can't stick net.IPNet in a Go hashmap. Bummer!
for _, addr := range addresses {
ip := addr.IP
ipNetAddr := prefixToIPNet(addr)
ip := ipNetAddr.IP
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}
mA, _ := addr.Mask.Size()
mA, _ := ipNetAddr.Mask.Size()
mB, _ := a.Mask.Size()
if bytes.Equal(ip, a.IP) && mA == mB {
return true
@@ -51,11 +53,55 @@ func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, add
ipnet := net.IPNet{IP: ip, Mask: net.CIDRMask(int(address.OnLinkPrefixLength), 8*len(ip))}
if includedInAddresses(ipnet) {
log.Printf("Cleaning up stale address %s from interface %s", ipnet.String(), iface.FriendlyName())
iface.LUID.DeleteIPAddress(ipnet)
addr, _ := netip.AddrFromSlice(ip)
b := int(bits(ip))
iface.LUID.DeleteIPAddress(netip.PrefixFrom(addr, b) )
}
}
}
}
func bits(ip net.IP) uint8 {
if ip.To4() != nil {
return 32
}
return 128
}
func prefixBits(prefix netip.Prefix) uint8 {
bits := uint8(32)
if prefix.Addr().Is6() {
bits = 128
}
return bits
}
func prefixToIP(prefix netip.Prefix) net.IP {
s := prefix.Addr().AsSlice()
return net.IP(s)
}
func prefixToMask(prefix netip.Prefix) net.IPMask {
return prefixToIPNet(prefix).Mask
}
func prefixToIPNet(prefix netip.Prefix) net.IPNet{
ip := prefixToIP(prefix)
return net.IPNet{
IP: ip,
Mask: net.CIDRMask(prefix.Bits(), int(bits(ip))),
}
}
func maskPrefix(prefix netip.Prefix) netip.Prefix{
ip := prefixToIP(prefix)
b := int(bits(ip))
mask := net.CIDRMask(int(prefix.Bits()), b)
for i := 0; i < b/8; i++ {
ip[i] &= mask[i]
}
addr, _ := netip.AddrFromSlice(ip)
return netip.PrefixFrom(addr, b)
}
func configureInterface(family winipcfg.AddressFamily, conf *conf.Config, tun *tun.NativeTun) error {
luid := winipcfg.LUID(tun.LUID())
@@ -65,13 +111,12 @@ func configureInterface(family winipcfg.AddressFamily, conf *conf.Config, tun *t
estimatedRouteCount += len(peer.AllowedIPs)
}
routes := make([]winipcfg.RouteData, 0, estimatedRouteCount)
addresses := make([]net.IPNet, len(conf.Interface.Addresses))
addresses := make([]netip.Prefix, len(conf.Interface.Addresses))
var haveV4Address, haveV6Address bool
for i, addr := range conf.Interface.Addresses {
addresses[i] = addr.IPNet()
if addr.Bits() == 32 {
for _, addr := range conf.Interface.Addresses {
if prefixBits(addr) == 32 {
haveV4Address = true
} else if addr.Bits() == 128 {
} else if prefixBits(addr) == 128 {
haveV6Address = true
}
}
@@ -80,24 +125,27 @@ func configureInterface(family winipcfg.AddressFamily, conf *conf.Config, tun *t
foundDefault6 := false
for _, peer := range conf.Peers {
for _, allowedip := range peer.AllowedIPs {
allowedip.MaskSelf()
if (allowedip.Bits() == 32 && !haveV4Address) || (allowedip.Bits() == 128 && !haveV6Address) {
allowedip = maskPrefix(allowedip)
pBits := prefixBits(allowedip)
if (pBits == 32 && !haveV4Address) || (pBits == 128 && !haveV6Address) {
continue
}
route := winipcfg.RouteData{
Destination: allowedip.IPNet(),
Destination: allowedip,
Metric: 0,
}
if allowedip.Bits() == 32 {
if allowedip.Cidr == 0 {
if pBits == 32 {
if allowedip.Bits() == 0 {
foundDefault4 = true
}
route.NextHop = net.IPv4zero
} else if allowedip.Bits() == 128 {
if allowedip.Cidr == 0 {
ipv4Zero, _ := netip.ParseAddr("0.0.0.0")
route.NextHop = ipv4Zero
} else if pBits == 128 {
if allowedip.Bits() == 0 {
foundDefault6 = true
}
route.NextHop = net.IPv6zero
ipv6Zero, _ := netip.ParseAddr("0000:0000:0000:0000:0000:0000:0000:0000")
route.NextHop = ipv6Zero
}
routes = append(routes, route)
}
@@ -117,22 +165,22 @@ func configureInterface(family winipcfg.AddressFamily, conf *conf.Config, tun *t
if routes[i].Metric != routes[j].Metric {
return routes[i].Metric < routes[j].Metric
}
if c := bytes.Compare(routes[i].NextHop, routes[j].NextHop); c != 0 {
if c := routes[i].NextHop.Compare(routes[j].NextHop); c != 0 {
return c < 0
}
if c := bytes.Compare(routes[i].Destination.IP, routes[j].Destination.IP); c != 0 {
if c := routes[i].Destination.Addr().Compare(routes[j].Destination.Addr()); c != 0 {
return c < 0
}
if c := bytes.Compare(routes[i].Destination.Mask, routes[j].Destination.Mask); c != 0 {
if c := bytes.Compare(prefixToMask(routes[i].Destination), prefixToMask(routes[j].Destination)); c != 0 {
return c < 0
}
return false
})
for i := 0; i < len(routes); i++ {
if i > 0 && routes[i].Metric == routes[i-1].Metric &&
bytes.Equal(routes[i].NextHop, routes[i-1].NextHop) &&
bytes.Equal(routes[i].Destination.IP, routes[i-1].Destination.IP) &&
bytes.Equal(routes[i].Destination.Mask, routes[i-1].Destination.Mask) {
(routes[i].NextHop.Compare(routes[i-1].NextHop) == 0) &&
(routes[i].Destination.Addr().Compare(routes[i-1].Destination.Addr()) == 0) &&
bytes.Equal(prefixToMask(routes[i].Destination), prefixToMask(routes[i-1].Destination)) {
continue
}
deduplicatedRoutes = append(deduplicatedRoutes, &routes[i])
@@ -174,8 +222,8 @@ func enableFirewall(conf *conf.Config, tun *tun.NativeTun) error {
if len(conf.Peers) == 1 && !conf.Interface.TableOff {
nextallowedip:
for _, allowedip := range conf.Peers[0].AllowedIPs {
if allowedip.Cidr == 0 {
for _, b := range allowedip.IP {
if allowedip.Bits() == 0 {
for _, b := range prefixToIP(allowedip) {
if b != 0 {
continue nextallowedip
}

View File

@@ -13,7 +13,7 @@ if exist .deps\prepared goto :build
rmdir /s /q .deps 2> NUL
mkdir .deps || goto :error
cd .deps || goto :error
call :download go.zip https://go.dev/dl/go1.18.8.windows-amd64.zip 980788761e75ed33ffc4f2a7a3ff07cd90949bd023eb1a8d855ef0b5de9cbcba || goto :error
call :download go.zip https://go.dev/dl/go1.20.8.windows-amd64.zip 6308336f7060023f2c9c58cdeefaa1389b5c4a96b4bd87b6ad742c57f8ac00da || goto :error
rem Mirror of https://github.com/mstorsjo/llvm-mingw/releases/download/20201020/llvm-mingw-20201020-msvcrt-x86_64.zip
call :download llvm-mingw-msvcrt.zip https://download.wireguard.com/windows-toolchain/distfiles/llvm-mingw-20201020-msvcrt-x86_64.zip 2e46593245090df96d15e360e092f0b62b97e93866e0162dca7f93b16722b844 || goto :error
call :download wintun.zip https://www.wintun.net/builds/wintun-0.12.zip eba90e26686ed86595ae0a6d4d3f4f022924b1758f5148a32a91c60cc6e604df || goto :error

View File

@@ -83,10 +83,10 @@ func deterministicGUID(c *conf.Config) *windows.GUID {
if bi, bj := sortedAllowedIPs[i].Bits(), sortedAllowedIPs[j].Bits(); bi != bj {
return bi < bj
}
if sortedAllowedIPs[i].Cidr != sortedAllowedIPs[j].Cidr {
return sortedAllowedIPs[i].Cidr < sortedAllowedIPs[j].Cidr
if sortedAllowedIPs[i].Bits() != sortedAllowedIPs[j].Bits() {
return sortedAllowedIPs[i].Bits() < sortedAllowedIPs[j].Bits()
}
return bytes.Compare(sortedAllowedIPs[i].IP[:], sortedAllowedIPs[j].IP[:]) < 0
return sortedAllowedIPs[i].Addr().Compare(sortedAllowedIPs[j].Addr()) < 0
})
for _, allowedip := range sortedAllowedIPs {
b2String(allowedip.String())

10
go.mod
View File

@@ -1,18 +1,16 @@
module github.com/amnezia-vpn/awg-windows
go 1.18
go 1.20
require (
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
github.com/amnezia-vpn/amnezia-wg v0.1.0
golang.org/x/crypto v0.12.0
golang.org/x/sys v0.11.0
golang.org/x/text v0.12.0
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2
golang.zx2c4.com/wireguard/windows v0.5.3
)
require (
github.com/tevino/abool/v2 v2.1.0 // indirect
golang.org/x/net v0.10.0 // indirect
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
)

33
go.sum
View File

@@ -1,32 +1,17 @@
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
github.com/amnezia-vpn/amnezia-wg v0.1.0 h1:a3plpoJSWbD+N8cK78vu/LEy4TQWYh7/YClpWZC4S4Y=
github.com/amnezia-vpn/amnezia-wg v0.1.0/go.mod h1:y3O3XsXzUXQYDwRNulQbnnAAhsjPYCnBjGmb2Daes64=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c=
github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2 h1:wfOOSvHgIzTZ9h5Vb6yUFZNn7uf3bT7PeYsHOO7tYDM=
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=

View File

@@ -12,8 +12,8 @@ import (
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/tun"
"github.com/amnezia-vpn/amnezia-wg/conn"
"github.com/amnezia-vpn/amnezia-wg/tun"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/services"

View File

@@ -8,7 +8,7 @@ package main
import (
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/ipc"
"github.com/amnezia-vpn/amnezia-wg/ipc"
"github.com/amnezia-vpn/awg-windows/conf"
)

View File

@@ -14,13 +14,13 @@ import (
"runtime"
"time"
"github.com/amnezia-vpn/amnezia-wg/conn"
"github.com/amnezia-vpn/amnezia-wg/device"
"github.com/amnezia-vpn/amnezia-wg/ipc"
"github.com/amnezia-vpn/amnezia-wg/tun"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/ipc"
"golang.zx2c4.com/wireguard/tun"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/elevate"
@@ -104,7 +104,7 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest,
log.Println("Shutting down")
}()
err = ringlogger.InitGlobalLogger("TUN")
err = ringlogger.InitGlobalLogger("TUN", "awg")
if err != nil {
serviceError = services.ErrorRingloggerOpen
return

View File

@@ -31,6 +31,8 @@ const (
ErrorDropPrivileges
ErrorRunScript
ErrorWin32
ErrorCreateWintun
ErrorUAPIListen
)
func (e Error) Error() string {
@@ -69,6 +71,10 @@ func (e Error) Error() string {
return "An error occurred while running a configuration script command"
case ErrorWin32:
return "An internal Windows error has occurred"
case ErrorUAPIListen:
return "Unable to listen on named pipe"
case ErrorCreateWintun:
return "Unable to create Wintun interface"
default:
return "An unknown error has occurred"
}

View File

@@ -1,26 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package services
import (
"errors"
"github.com/amnezia-vpn/awg-windows/conf"
)
func ServiceNameOfTunnel(tunnelName string) (string, error) {
if !conf.TunnelNameIsValid(tunnelName) {
return "", errors.New("Tunnel name is not valid")
}
return "WireGuardTunnel$" + tunnelName, nil
}
func PipePathOfTunnel(tunnelName string) (string, error) {
if !conf.TunnelNameIsValid(tunnelName) {
return "", errors.New("Tunnel name is not valid")
}
return `\\.\pipe\ProtectedPrefix\Administrators\WireGuard\` + tunnelName, nil
}

View File

@@ -10,9 +10,9 @@ import (
"sync"
"time"
"github.com/amnezia-vpn/amnezia-wg/conn"
"github.com/amnezia-vpn/amnezia-wg/tun"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/tun"
"github.com/amnezia-vpn/awg-windows/tunnel/winipcfg"
)

View File

@@ -8,7 +8,7 @@ package tunnel
import (
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/ipc"
"github.com/amnezia-vpn/amnezia-wg/ipc"
"github.com/amnezia-vpn/awg-windows/conf"
)

View File

@@ -1,150 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"runtime"
"strings"
"github.com/amnezia-vpn/awg-windows/driver"
"github.com/lxn/walk"
"github.com/lxn/win"
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/awg-windows/l18n"
"github.com/amnezia-vpn/awg-windows/version"
)
var (
easterEggIndex = -1
showingAboutDialog *walk.Dialog
)
func onAbout(owner walk.Form) {
showError(runAboutDialog(owner), owner)
}
func runAboutDialog(owner walk.Form) error {
if showingAboutDialog != nil {
showingAboutDialog.Show()
raise(showingAboutDialog.Handle())
return nil
}
vbl := walk.NewVBoxLayout()
vbl.SetMargins(walk.Margins{80, 20, 80, 20})
vbl.SetSpacing(10)
var disposables walk.Disposables
defer disposables.Treat()
var err error
showingAboutDialog, err = walk.NewDialogWithFixedSize(owner)
if err != nil {
return err
}
defer func() {
showingAboutDialog = nil
}()
disposables.Add(showingAboutDialog)
showingAboutDialog.SetTitle(l18n.Sprintf("About WireGuard"))
showingAboutDialog.SetLayout(vbl)
if icon, err := loadLogoIcon(32); err == nil {
showingAboutDialog.SetIcon(icon)
}
font, _ := walk.NewFont("Segoe UI", 9, 0)
showingAboutDialog.SetFont(font)
iv, err := walk.NewImageView(showingAboutDialog)
if err != nil {
return err
}
iv.SetCursor(walk.CursorHand())
iv.MouseUp().Attach(func(x, y int, button walk.MouseButton) {
if button == walk.LeftButton {
win.ShellExecute(showingAboutDialog.Handle(), nil, windows.StringToUTF16Ptr("https://www.wireguard.com/"), nil, nil, win.SW_SHOWNORMAL)
} else if easterEggIndex >= 0 && button == walk.RightButton {
if icon, err := loadSystemIcon("moricons", int32(easterEggIndex), 128); err == nil {
iv.SetImage(icon)
easterEggIndex++
} else {
easterEggIndex = -1
if logo, err := loadLogoIcon(128); err == nil {
iv.SetImage(logo)
}
}
}
})
if logo, err := loadLogoIcon(128); err == nil {
iv.SetImage(logo)
}
iv.Accessibility().SetName(l18n.Sprintf("WireGuard logo image"))
wgLbl, err := walk.NewTextLabel(showingAboutDialog)
if err != nil {
return err
}
wgFont, _ := walk.NewFont("Segoe UI", 16, walk.FontBold)
wgLbl.SetFont(wgFont)
wgLbl.SetTextAlignment(walk.AlignHCenterVNear)
wgLbl.SetText("WireGuard")
detailsLbl, err := walk.NewTextLabel(showingAboutDialog)
if err != nil {
return err
}
detailsLbl.SetTextAlignment(walk.AlignHCenterVNear)
detailsLbl.SetText(l18n.Sprintf("App version: %s\nDriver version: %s\nGo version: %s\nOperating system: %s\nArchitecture: %s", version.Number, driver.Version(), strings.TrimPrefix(runtime.Version(), "go"), version.OsName(), version.Arch()))
copyrightLbl, err := walk.NewTextLabel(showingAboutDialog)
if err != nil {
return err
}
copyrightFont, _ := walk.NewFont("Segoe UI", 7, 0)
copyrightLbl.SetFont(copyrightFont)
copyrightLbl.SetTextAlignment(walk.AlignHCenterVNear)
copyrightLbl.SetText("Copyright © 2015-2021 Jason A. Donenfeld. All Rights Reserved.")
buttonCP, err := walk.NewComposite(showingAboutDialog)
if err != nil {
return err
}
hbl := walk.NewHBoxLayout()
hbl.SetMargins(walk.Margins{VNear: 10})
buttonCP.SetLayout(hbl)
walk.NewHSpacer(buttonCP)
closePB, err := walk.NewPushButton(buttonCP)
if err != nil {
return err
}
closePB.SetAlignment(walk.AlignHCenterVNear)
closePB.SetText(l18n.Sprintf("Close"))
closePB.Clicked().Attach(showingAboutDialog.Accept)
donatePB, err := walk.NewPushButton(buttonCP)
if err != nil {
return err
}
donatePB.SetAlignment(walk.AlignHCenterVNear)
donatePB.SetText(l18n.Sprintf("♥ &Donate!"))
donatePB.Clicked().Attach(func() {
if easterEggIndex == -1 {
easterEggIndex = 0
}
win.ShellExecute(showingAboutDialog.Handle(), nil, windows.StringToUTF16Ptr("https://www.wireguard.com/donations/"), nil, nil, win.SW_SHOWNORMAL)
showingAboutDialog.Accept()
})
walk.NewHSpacer(buttonCP)
showingAboutDialog.SetDefaultButton(donatePB)
showingAboutDialog.SetCancelButton(closePB)
disposables.Spare()
showingAboutDialog.Run()
return nil
}

View File

@@ -1,799 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"strconv"
"strings"
"time"
"github.com/lxn/walk"
"github.com/lxn/win"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/l18n"
"golang.zx2c4.com/wireguard/windows/manager"
)
type widgetsLine interface {
widgets() (walk.Widget, walk.Widget)
}
type widgetsLinesView interface {
widgetsLines() []widgetsLine
}
type labelStatusLine struct {
label *walk.TextLabel
statusComposite *walk.Composite
statusImage *walk.ImageView
statusLabel *walk.LineEdit
}
type labelTextLine struct {
label *walk.TextLabel
text *walk.TextEdit
}
type toggleActiveLine struct {
composite *walk.Composite
button *walk.PushButton
}
type interfaceView struct {
status *labelStatusLine
publicKey *labelTextLine
listenPort *labelTextLine
junkPacketCount *labelTextLine
junkPacketMinSize *labelTextLine
junkPacketMaxSize *labelTextLine
initPacketJunkSize *labelTextLine
responsePacketJunkSize *labelTextLine
initPacketMagicHeader *labelTextLine
responsePacketMagicHeader *labelTextLine
underloadPacketMagicHeader *labelTextLine
transportPacketMagicHeader *labelTextLine
mtu *labelTextLine
addresses *labelTextLine
dns *labelTextLine
scripts *labelTextLine
table *labelTextLine
toggleActive *toggleActiveLine
lines []widgetsLine
}
type peerView struct {
publicKey *labelTextLine
presharedKey *labelTextLine
allowedIPs *labelTextLine
endpoint *labelTextLine
persistentKeepalive *labelTextLine
latestHandshake *labelTextLine
transfer *labelTextLine
lines []widgetsLine
}
type ConfView struct {
*walk.ScrollView
name *walk.GroupBox
interfaze *interfaceView
peers map[conf.Key]*peerView
tunnelChangedCB *manager.TunnelChangeCallback
tunnel *manager.Tunnel
updateTicker *time.Ticker
}
func (lsl *labelStatusLine) widgets() (walk.Widget, walk.Widget) {
return lsl.label, lsl.statusComposite
}
func (lsl *labelStatusLine) update(state manager.TunnelState) {
icon, err := iconForState(state, 14)
if err == nil {
lsl.statusImage.SetImage(icon)
} else {
lsl.statusImage.SetImage(nil)
}
s, e := lsl.statusLabel.TextSelection()
lsl.statusLabel.SetText(textForState(state, false))
lsl.statusLabel.SetTextSelection(s, e)
}
func (lsl *labelStatusLine) Dispose() {
lsl.label.Dispose()
lsl.statusComposite.Dispose()
}
func newLabelStatusLine(parent walk.Container) (*labelStatusLine, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
lsl := new(labelStatusLine)
if lsl.label, err = walk.NewTextLabel(parent); err != nil {
return nil, err
}
disposables.Add(lsl.label)
lsl.label.SetText(l18n.Sprintf("Status:"))
lsl.label.SetTextAlignment(walk.AlignHFarVNear)
if lsl.statusComposite, err = walk.NewComposite(parent); err != nil {
return nil, err
}
disposables.Add(lsl.statusComposite)
layout := walk.NewHBoxLayout()
layout.SetMargins(walk.Margins{})
layout.SetAlignment(walk.AlignHNearVNear)
layout.SetSpacing(0)
lsl.statusComposite.SetLayout(layout)
if lsl.statusImage, err = walk.NewImageView(lsl.statusComposite); err != nil {
return nil, err
}
disposables.Add(lsl.statusImage)
lsl.statusImage.SetMargin(2)
lsl.statusImage.SetMode(walk.ImageViewModeIdeal)
if lsl.statusLabel, err = walk.NewLineEdit(lsl.statusComposite); err != nil {
return nil, err
}
disposables.Add(lsl.statusLabel)
win.SetWindowLong(lsl.statusLabel.Handle(), win.GWL_EXSTYLE, win.GetWindowLong(lsl.statusLabel.Handle(), win.GWL_EXSTYLE)&^win.WS_EX_CLIENTEDGE)
lsl.statusLabel.SetReadOnly(true)
lsl.statusLabel.SetBackground(walk.NullBrush())
lsl.statusLabel.FocusedChanged().Attach(func() {
lsl.statusLabel.SetTextSelection(0, 0)
})
lsl.update(manager.TunnelUnknown)
lsl.statusLabel.Accessibility().SetRole(walk.AccRoleStatictext)
disposables.Spare()
return lsl, nil
}
func (lt *labelTextLine) widgets() (walk.Widget, walk.Widget) {
return lt.label, lt.text
}
func (lt *labelTextLine) show(text string) {
s, e := lt.text.TextSelection()
lt.text.SetText(text)
lt.label.SetVisible(true)
lt.text.SetVisible(true)
lt.text.SetTextSelection(s, e)
}
func (lt *labelTextLine) hide() {
lt.text.SetText("")
lt.label.SetVisible(false)
lt.text.SetVisible(false)
}
func (lt *labelTextLine) Dispose() {
lt.label.Dispose()
lt.text.Dispose()
}
func newLabelTextLine(fieldName string, parent walk.Container) (*labelTextLine, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
lt := new(labelTextLine)
if lt.label, err = walk.NewTextLabel(parent); err != nil {
return nil, err
}
disposables.Add(lt.label)
lt.label.SetText(fieldName)
lt.label.SetTextAlignment(walk.AlignHFarVNear)
lt.label.SetVisible(false)
if lt.text, err = walk.NewTextEdit(parent); err != nil {
return nil, err
}
disposables.Add(lt.text)
win.SetWindowLong(lt.text.Handle(), win.GWL_EXSTYLE, win.GetWindowLong(lt.text.Handle(), win.GWL_EXSTYLE)&^win.WS_EX_CLIENTEDGE)
lt.text.SetCompactHeight(true)
lt.text.SetReadOnly(true)
lt.text.SetBackground(walk.NullBrush())
lt.text.SetVisible(false)
lt.text.FocusedChanged().Attach(func() {
lt.text.SetTextSelection(0, 0)
})
lt.text.Accessibility().SetRole(walk.AccRoleStatictext)
disposables.Spare()
return lt, nil
}
func (tal *toggleActiveLine) widgets() (walk.Widget, walk.Widget) {
return nil, tal.composite
}
func (tal *toggleActiveLine) updateGlobal(globalState manager.TunnelState) {
tal.button.SetEnabled(globalState == manager.TunnelStarted || globalState == manager.TunnelStopped)
}
func (tal *toggleActiveLine) update(state manager.TunnelState) {
var text string
switch state {
case manager.TunnelStarted:
text = l18n.Sprintf("&Deactivate")
case manager.TunnelStopped:
text = l18n.Sprintf("&Activate")
case manager.TunnelStarting, manager.TunnelStopping:
text = textForState(state, true)
default:
text = ""
}
tal.button.SetText(text)
tal.button.SetVisible(state != manager.TunnelUnknown)
}
func (tal *toggleActiveLine) Dispose() {
tal.composite.Dispose()
}
func newToggleActiveLine(parent walk.Container) (*toggleActiveLine, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
tal := new(toggleActiveLine)
if tal.composite, err = walk.NewComposite(parent); err != nil {
return nil, err
}
disposables.Add(tal.composite)
layout := walk.NewHBoxLayout()
layout.SetMargins(walk.Margins{0, 0, 0, 6})
tal.composite.SetLayout(layout)
if tal.button, err = walk.NewPushButton(tal.composite); err != nil {
return nil, err
}
disposables.Add(tal.button)
walk.NewHSpacer(tal.composite)
tal.update(manager.TunnelStopped)
disposables.Spare()
return tal, nil
}
type labelTextLineItem struct {
label string
ptr **labelTextLine
}
func createLabelTextLines(items []labelTextLineItem, parent walk.Container, disposables *walk.Disposables) ([]widgetsLine, error) {
var err error
var disps walk.Disposables
defer disps.Treat()
wls := make([]widgetsLine, len(items))
for i, item := range items {
if *item.ptr, err = newLabelTextLine(item.label, parent); err != nil {
return nil, err
}
disps.Add(*item.ptr)
if disposables != nil {
disposables.Add(*item.ptr)
}
wls[i] = *item.ptr
}
disps.Spare()
return wls, nil
}
func newInterfaceView(parent walk.Container) (*interfaceView, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
iv := new(interfaceView)
if iv.status, err = newLabelStatusLine(parent); err != nil {
return nil, err
}
disposables.Add(iv.status)
items := []labelTextLineItem{
{l18n.Sprintf("Public key:"), &iv.publicKey},
{l18n.Sprintf("Listen port:"), &iv.listenPort},
{l18n.Sprintf("Jc:"), &iv.junkPacketCount},
{l18n.Sprintf("Jmin:"), &iv.junkPacketMinSize},
{l18n.Sprintf("Jmax:"), &iv.junkPacketMaxSize},
{l18n.Sprintf("S1:"), &iv.initPacketJunkSize},
{l18n.Sprintf("S2:"), &iv.responsePacketJunkSize},
{l18n.Sprintf("H1:"), &iv.initPacketMagicHeader},
{l18n.Sprintf("H2:"), &iv.responsePacketMagicHeader},
{l18n.Sprintf("H3:"), &iv.underloadPacketMagicHeader},
{l18n.Sprintf("H4:"), &iv.transportPacketMagicHeader},
{l18n.Sprintf("MTU:"), &iv.mtu},
{l18n.Sprintf("Addresses:"), &iv.addresses},
{l18n.Sprintf("DNS servers:"), &iv.dns},
{l18n.Sprintf("Scripts:"), &iv.scripts},
{l18n.Sprintf("Table:"), &iv.table},
}
if iv.lines, err = createLabelTextLines(items, parent, &disposables); err != nil {
return nil, err
}
if iv.toggleActive, err = newToggleActiveLine(parent); err != nil {
return nil, err
}
disposables.Add(iv.toggleActive)
iv.lines = append([]widgetsLine{iv.status}, append(iv.lines, iv.toggleActive)...)
layoutInGrid(iv, parent.Layout().(*walk.GridLayout))
disposables.Spare()
return iv, nil
}
func newPeerView(parent walk.Container) (*peerView, error) {
pv := new(peerView)
items := []labelTextLineItem{
{l18n.Sprintf("Public key:"), &pv.publicKey},
{l18n.Sprintf("Preshared key:"), &pv.presharedKey},
{l18n.Sprintf("Allowed IPs:"), &pv.allowedIPs},
{l18n.Sprintf("Endpoint:"), &pv.endpoint},
{l18n.Sprintf("Persistent keepalive:"), &pv.persistentKeepalive},
{l18n.Sprintf("Latest handshake:"), &pv.latestHandshake},
{l18n.Sprintf("Transfer:"), &pv.transfer},
}
var err error
if pv.lines, err = createLabelTextLines(items, parent, nil); err != nil {
return nil, err
}
layoutInGrid(pv, parent.Layout().(*walk.GridLayout))
return pv, nil
}
func layoutInGrid(view widgetsLinesView, layout *walk.GridLayout) {
for i, l := range view.widgetsLines() {
w1, w2 := l.widgets()
if w1 != nil {
layout.SetRange(w1, walk.Rectangle{0, i, 1, 1})
}
if w2 != nil {
layout.SetRange(w2, walk.Rectangle{2, i, 1, 1})
}
}
}
func (iv *interfaceView) widgetsLines() []widgetsLine {
return iv.lines
}
func (iv *interfaceView) apply(c *conf.Interface) {
if IsAdmin {
iv.publicKey.show(c.PrivateKey.Public().String())
} else {
iv.publicKey.hide()
}
if c.ListenPort > 0 {
iv.listenPort.show(strconv.Itoa(int(c.ListenPort)))
} else {
iv.listenPort.hide()
}
if c.JunkPacketCount > 0 {
iv.junkPacketCount.show(strconv.Itoa(int(c.JunkPacketCount)))
} else {
iv.junkPacketCount.hide()
}
if c.JunkPacketMinSize > 0 {
iv.junkPacketMinSize.show(strconv.Itoa(int(c.JunkPacketMinSize)))
} else {
iv.junkPacketMinSize.hide()
}
if c.JunkPacketMaxSize > 0 {
iv.junkPacketMaxSize.show(strconv.Itoa(int(c.JunkPacketMaxSize)))
} else {
iv.junkPacketMaxSize.hide()
}
if c.InitPacketJunkSize > 0 {
iv.initPacketJunkSize.show(strconv.Itoa(int(c.InitPacketJunkSize)))
} else {
iv.initPacketJunkSize.hide()
}
if c.ResponsePacketJunkSize > 0 {
iv.responsePacketJunkSize.show(strconv.Itoa(int(c.ResponsePacketJunkSize)))
} else {
iv.responsePacketJunkSize.hide()
}
if c.InitPacketMagicHeader > 0 {
iv.initPacketMagicHeader.show(strconv.FormatUint(uint64(c.InitPacketMagicHeader), 10))
} else {
iv.initPacketMagicHeader.hide()
}
if c.ResponsePacketMagicHeader > 0 {
iv.responsePacketMagicHeader.show(strconv.FormatUint(uint64(c.ResponsePacketMagicHeader), 10))
} else {
iv.responsePacketMagicHeader.hide()
}
if c.UnderloadPacketMagicHeader > 0 {
iv.underloadPacketMagicHeader.show(strconv.FormatUint(uint64(c.UnderloadPacketMagicHeader), 10))
} else {
iv.underloadPacketMagicHeader.hide()
}
if c.TransportPacketMagicHeader > 0 {
iv.transportPacketMagicHeader.show(strconv.FormatUint(uint64(c.TransportPacketMagicHeader), 10))
} else {
iv.transportPacketMagicHeader.hide()
}
if c.MTU > 0 {
iv.mtu.show(strconv.Itoa(int(c.MTU)))
} else {
iv.mtu.hide()
}
if len(c.Addresses) > 0 {
addrStrings := make([]string, len(c.Addresses))
for i, address := range c.Addresses {
addrStrings[i] = address.String()
}
iv.addresses.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator()))
} else {
iv.addresses.hide()
}
if len(c.DNS)+len(c.DNSSearch) > 0 {
addrStrings := make([]string, 0, len(c.DNS)+len(c.DNSSearch))
for _, address := range c.DNS {
addrStrings = append(addrStrings, address.String())
}
addrStrings = append(addrStrings, c.DNSSearch...)
iv.dns.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator()))
} else {
iv.dns.hide()
}
var scriptsInUse []string
if len(c.PreUp) > 0 {
scriptsInUse = append(scriptsInUse, l18n.Sprintf("pre-up"))
}
if len(c.PostUp) > 0 {
scriptsInUse = append(scriptsInUse, l18n.Sprintf("post-up"))
}
if len(c.PreDown) > 0 {
scriptsInUse = append(scriptsInUse, l18n.Sprintf("pre-down"))
}
if len(c.PostDown) > 0 {
scriptsInUse = append(scriptsInUse, l18n.Sprintf("post-down"))
}
if len(scriptsInUse) > 0 {
if conf.AdminBool("DangerousScriptExecution") {
iv.scripts.show(strings.Join(scriptsInUse, l18n.EnumerationSeparator()))
} else {
iv.scripts.show(l18n.Sprintf("disabled, per policy"))
}
} else {
iv.scripts.hide()
}
if c.TableOff {
iv.table.show(l18n.Sprintf("off"))
} else {
iv.table.hide()
}
}
func (pv *peerView) widgetsLines() []widgetsLine {
return pv.lines
}
func (pv *peerView) apply(c *conf.Peer) {
if IsAdmin {
pv.publicKey.show(c.PublicKey.String())
} else {
pv.publicKey.hide()
}
if !c.PresharedKey.IsZero() && IsAdmin {
pv.presharedKey.show(l18n.Sprintf("enabled"))
} else {
pv.presharedKey.hide()
}
if len(c.AllowedIPs) > 0 {
addrStrings := make([]string, len(c.AllowedIPs))
for i, address := range c.AllowedIPs {
addrStrings[i] = address.String()
}
pv.allowedIPs.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator()))
} else {
pv.allowedIPs.hide()
}
if !c.Endpoint.IsEmpty() {
pv.endpoint.show(c.Endpoint.String())
} else {
pv.endpoint.hide()
}
if c.PersistentKeepalive > 0 {
pv.persistentKeepalive.show(strconv.Itoa(int(c.PersistentKeepalive)))
} else {
pv.persistentKeepalive.hide()
}
if !c.LastHandshakeTime.IsEmpty() {
pv.latestHandshake.show(c.LastHandshakeTime.String())
} else {
pv.latestHandshake.hide()
}
if c.RxBytes > 0 || c.TxBytes > 0 {
pv.transfer.show(l18n.Sprintf("%s received, %s sent", c.RxBytes.String(), c.TxBytes.String()))
} else {
pv.transfer.hide()
}
}
func newPaddedGroupGrid(parent walk.Container) (group *walk.GroupBox, err error) {
group, err = walk.NewGroupBox(parent)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
group.Dispose()
}
}()
layout := walk.NewGridLayout()
layout.SetMargins(walk.Margins{10, 5, 10, 5})
layout.SetSpacing(0)
err = group.SetLayout(layout)
if err != nil {
return nil, err
}
spacer, err := walk.NewSpacerWithCfg(group, &walk.SpacerCfg{walk.GrowableHorz | walk.GreedyHorz, walk.Size{10, 0}, false})
if err != nil {
return nil, err
}
layout.SetRange(spacer, walk.Rectangle{1, 0, 1, 1})
return group, nil
}
func NewConfView(parent walk.Container) (*ConfView, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
cv := new(ConfView)
if cv.ScrollView, err = walk.NewScrollView(parent); err != nil {
return nil, err
}
disposables.Add(cv)
vlayout := walk.NewVBoxLayout()
vlayout.SetMargins(walk.Margins{5, 0, 5, 0})
cv.SetLayout(vlayout)
if cv.name, err = newPaddedGroupGrid(cv); err != nil {
return nil, err
}
if cv.interfaze, err = newInterfaceView(cv.name); err != nil {
return nil, err
}
cv.interfaze.toggleActive.button.Clicked().Attach(cv.onToggleActiveClicked)
cv.peers = make(map[conf.Key]*peerView)
cv.tunnelChangedCB = manager.IPCClientRegisterTunnelChange(cv.onTunnelChanged)
cv.SetTunnel(nil)
globalState, err := manager.IPCClientGlobalState()
if err != nil {
return nil, err
}
cv.interfaze.toggleActive.updateGlobal(globalState)
if err := walk.InitWrapperWindow(cv); err != nil {
return nil, err
}
cv.SetDoubleBuffering(true)
cv.updateTicker = time.NewTicker(time.Second)
go func() {
for range cv.updateTicker.C {
if !cv.Visible() || !cv.Form().Visible() || win.IsIconic(cv.Form().Handle()) {
continue
}
if cv.tunnel != nil {
tunnel := cv.tunnel
var state manager.TunnelState
var config conf.Config
if state, _ = tunnel.State(); state == manager.TunnelStarted {
config, _ = tunnel.RuntimeConfig()
}
if config.Name == "" {
config, _ = tunnel.StoredConfig()
}
cv.Synchronize(func() {
cv.setTunnel(tunnel, &config, state)
})
}
}
}()
disposables.Spare()
return cv, nil
}
func (cv *ConfView) Dispose() {
if cv.tunnelChangedCB != nil {
cv.tunnelChangedCB.Unregister()
cv.tunnelChangedCB = nil
}
if cv.updateTicker != nil {
cv.updateTicker.Stop()
cv.updateTicker = nil
}
cv.ScrollView.Dispose()
}
func (cv *ConfView) onToggleActiveClicked() {
cv.interfaze.toggleActive.button.SetEnabled(false)
go func() {
oldState, err := cv.tunnel.Toggle()
if err != nil {
cv.Synchronize(func() {
if oldState == manager.TunnelUnknown {
showErrorCustom(cv.Form(), l18n.Sprintf("Failed to determine tunnel state"), err.Error())
} else if oldState == manager.TunnelStopped {
showErrorCustom(cv.Form(), l18n.Sprintf("Failed to activate tunnel"), err.Error())
} else if oldState == manager.TunnelStarted {
showErrorCustom(cv.Form(), l18n.Sprintf("Failed to deactivate tunnel"), err.Error())
}
})
}
}()
}
func (cv *ConfView) onTunnelChanged(tunnel *manager.Tunnel, state, globalState manager.TunnelState, err error) {
cv.Synchronize(func() {
cv.interfaze.toggleActive.updateGlobal(globalState)
if cv.tunnel != nil && cv.tunnel.Name == tunnel.Name {
cv.interfaze.status.update(state)
cv.interfaze.toggleActive.update(state)
}
})
if cv.tunnel != nil && cv.tunnel.Name == tunnel.Name {
var config conf.Config
if state == manager.TunnelStarted {
config, _ = tunnel.RuntimeConfig()
}
if config.Name == "" {
config, _ = tunnel.StoredConfig()
}
cv.Synchronize(func() {
cv.setTunnel(tunnel, &config, state)
})
}
}
func (cv *ConfView) SetTunnel(tunnel *manager.Tunnel) {
cv.tunnel = tunnel // XXX: This races with the read in the updateTicker, but it's pointer-sized!
var config conf.Config
var state manager.TunnelState
if tunnel != nil {
go func() {
if state, _ = tunnel.State(); state == manager.TunnelStarted {
config, _ = tunnel.RuntimeConfig()
}
if config.Name == "" {
config, _ = tunnel.StoredConfig()
}
cv.Synchronize(func() {
cv.setTunnel(tunnel, &config, state)
})
}()
} else {
cv.setTunnel(tunnel, &config, state)
}
}
func (cv *ConfView) setTunnel(tunnel *manager.Tunnel, config *conf.Config, state manager.TunnelState) {
if !(cv.tunnel == nil || tunnel == nil || tunnel.Name == cv.tunnel.Name) {
return
}
title := l18n.Sprintf("Interface: %s", config.Name)
if cv.name.Title() != title {
cv.SetSuspended(true)
defer cv.SetSuspended(false)
cv.name.SetTitle(title)
}
cv.name.SetVisible(tunnel != nil)
cv.interfaze.apply(&config.Interface)
cv.interfaze.status.update(state)
cv.interfaze.toggleActive.update(state)
inverse := make(map[*peerView]bool, len(cv.peers))
all := make([]*peerView, 0, len(cv.peers))
for _, pv := range cv.peers {
inverse[pv] = true
all = append(all, pv)
}
someMatch := false
for _, peer := range config.Peers {
_, ok := cv.peers[peer.PublicKey]
if ok {
someMatch = true
break
}
}
for _, peer := range config.Peers {
if pv := cv.peers[peer.PublicKey]; (!someMatch && len(all) > 0) || pv != nil {
if pv == nil {
pv = all[0]
all = all[1:]
k, e := conf.NewPrivateKeyFromString(pv.publicKey.text.Text())
if e != nil {
continue
}
delete(cv.peers, *k)
cv.peers[peer.PublicKey] = pv
}
pv.apply(&peer)
inverse[pv] = false
} else {
group, err := newPaddedGroupGrid(cv)
if err != nil {
continue
}
group.SetTitle(l18n.Sprintf("Peer"))
pv, err := newPeerView(group)
if err != nil {
group.Dispose()
continue
}
pv.apply(&peer)
cv.peers[peer.PublicKey] = pv
}
}
for pv, remove := range inverse {
if !remove {
continue
}
k, e := conf.NewPrivateKeyFromString(pv.publicKey.text.Text())
if e != nil {
continue
}
delete(cv.peers, *k)
groupBox := pv.publicKey.label.Parent().AsContainerBase().Parent().(*walk.GroupBox)
groupBox.SetVisible(false)
groupBox.Parent().Children().Remove(groupBox)
groupBox.Dispose()
}
}

View File

@@ -1,345 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"net/netip"
"strings"
"github.com/lxn/walk"
"github.com/lxn/win"
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/l18n"
"github.com/amnezia-vpn/awg-windows/ui/syntax"
"golang.zx2c4.com/wireguard/windows/manager"
)
type EditDialog struct {
*walk.Dialog
nameEdit *walk.LineEdit
pubkeyEdit *walk.LineEdit
syntaxEdit *syntax.SyntaxEdit
blockUntunneledTrafficCB *walk.CheckBox
saveButton *walk.PushButton
config conf.Config
lastPrivateKey string
blockUntunneledTraficCheckGuard bool
}
func runEditDialog(owner walk.Form, tunnel *manager.Tunnel) *conf.Config {
dlg, err := newEditDialog(owner, tunnel)
if showError(err, owner) {
return nil
}
if dlg.Run() == walk.DlgCmdOK {
return &dlg.config
}
return nil
}
func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
dlg := new(EditDialog)
var title string
if tunnel == nil {
title = l18n.Sprintf("Create new tunnel")
} else {
title = l18n.Sprintf("Edit tunnel")
}
if tunnel == nil {
// Creating a new tunnel, create a new private key and use the default template
pk, _ := conf.NewPrivateKey()
dlg.config = conf.Config{Interface: conf.Interface{PrivateKey: *pk}}
} else {
dlg.config, _ = tunnel.StoredConfig()
}
layout := walk.NewGridLayout()
layout.SetSpacing(6)
layout.SetMargins(walk.Margins{10, 10, 10, 10})
layout.SetColumnStretchFactor(1, 3)
if dlg.Dialog, err = walk.NewDialog(owner); err != nil {
return nil, err
}
disposables.Add(dlg)
dlg.SetIcon(owner.Icon())
dlg.SetTitle(title)
dlg.SetLayout(layout)
dlg.SetMinMaxSize(walk.Size{500, 400}, walk.Size{0, 0})
if icon, err := loadSystemIcon("imageres", -114, 32); err == nil {
dlg.SetIcon(icon)
}
nameLabel, err := walk.NewTextLabel(dlg)
if err != nil {
return nil, err
}
layout.SetRange(nameLabel, walk.Rectangle{0, 0, 1, 1})
nameLabel.SetTextAlignment(walk.AlignHFarVCenter)
nameLabel.SetText(l18n.Sprintf("&Name:"))
if dlg.nameEdit, err = walk.NewLineEdit(dlg); err != nil {
return nil, err
}
layout.SetRange(dlg.nameEdit, walk.Rectangle{1, 0, 1, 1})
dlg.nameEdit.SetText(dlg.config.Name)
pubkeyLabel, err := walk.NewTextLabel(dlg)
if err != nil {
return nil, err
}
layout.SetRange(pubkeyLabel, walk.Rectangle{0, 1, 1, 1})
pubkeyLabel.SetTextAlignment(walk.AlignHFarVCenter)
pubkeyLabel.SetText(l18n.Sprintf("&Public key:"))
if dlg.pubkeyEdit, err = walk.NewLineEdit(dlg); err != nil {
return nil, err
}
layout.SetRange(dlg.pubkeyEdit, walk.Rectangle{1, 1, 1, 1})
dlg.pubkeyEdit.SetReadOnly(true)
dlg.pubkeyEdit.SetText(l18n.Sprintf("(unknown)"))
dlg.pubkeyEdit.Accessibility().SetRole(walk.AccRoleStatictext)
if dlg.syntaxEdit, err = syntax.NewSyntaxEdit(dlg); err != nil {
return nil, err
}
layout.SetRange(dlg.syntaxEdit, walk.Rectangle{0, 2, 2, 1})
buttonsContainer, err := walk.NewComposite(dlg)
if err != nil {
return nil, err
}
layout.SetRange(buttonsContainer, walk.Rectangle{0, 3, 2, 1})
buttonsContainer.SetLayout(walk.NewHBoxLayout())
buttonsContainer.Layout().SetMargins(walk.Margins{})
if dlg.blockUntunneledTrafficCB, err = walk.NewCheckBox(buttonsContainer); err != nil {
return nil, err
}
dlg.blockUntunneledTrafficCB.SetText(l18n.Sprintf("&Block untunneled traffic (kill-switch)"))
dlg.blockUntunneledTrafficCB.SetToolTipText(l18n.Sprintf("When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, and the interface does not have table off, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface or is to the wrong DNS server, with special exceptions for DHCP and NDP."))
dlg.blockUntunneledTrafficCB.SetVisible(false)
dlg.blockUntunneledTrafficCB.CheckedChanged().Attach(dlg.onBlockUntunneledTrafficCBCheckedChanged)
walk.NewHSpacer(buttonsContainer)
if dlg.saveButton, err = walk.NewPushButton(buttonsContainer); err != nil {
return nil, err
}
dlg.saveButton.SetText(l18n.Sprintf("&Save"))
dlg.saveButton.Clicked().Attach(dlg.onSaveButtonClicked)
cancelButton, err := walk.NewPushButton(buttonsContainer)
if err != nil {
return nil, err
}
cancelButton.SetText(l18n.Sprintf("Cancel"))
cancelButton.Clicked().Attach(dlg.Cancel)
dlg.SetCancelButton(cancelButton)
dlg.SetDefaultButton(dlg.saveButton)
dlg.syntaxEdit.PrivateKeyChanged().Attach(dlg.onSyntaxEditPrivateKeyChanged)
dlg.syntaxEdit.BlockUntunneledTrafficStateChanged().Attach(dlg.onBlockUntunneledTrafficStateChanged)
dlg.syntaxEdit.SetText(dlg.config.ToWgQuick())
// Insert a dummy label immediately preceding syntaxEdit to have screen readers read it.
// Otherwise they fallback to "RichEdit Control".
syntaxEditWnd := dlg.syntaxEdit.Handle()
parentWnd := win.GetParent(syntaxEditWnd)
labelWnd := win.CreateWindowEx(0,
windows.StringToUTF16Ptr("STATIC"), windows.StringToUTF16Ptr(l18n.Sprintf("&Configuration:")),
win.WS_CHILD|win.WS_GROUP|win.SS_LEFT, 0, 0, 0, 0,
parentWnd, win.HMENU(^uintptr(0)), win.HINSTANCE(win.GetWindowLongPtr(parentWnd, win.GWLP_HINSTANCE)), nil)
prevWnd := win.GetWindow(syntaxEditWnd, win.GW_HWNDPREV)
nextWnd := win.GetWindow(syntaxEditWnd, win.GW_HWNDNEXT)
win.SetWindowPos(labelWnd, prevWnd, 0, 0, 0, 0, win.SWP_NOSIZE|win.SWP_NOMOVE)
win.SetWindowPos(syntaxEditWnd, labelWnd, 0, 0, 0, 0, win.SWP_NOSIZE|win.SWP_NOMOVE)
win.SetWindowPos(nextWnd, syntaxEditWnd, 0, 0, 0, 0, win.SWP_NOSIZE|win.SWP_NOMOVE)
if tunnel != nil {
dlg.Starting().Attach(func() {
dlg.syntaxEdit.SetFocus()
})
}
disposables.Spare()
return dlg, nil
}
func (dlg *EditDialog) onBlockUntunneledTrafficCBCheckedChanged() {
if dlg.blockUntunneledTraficCheckGuard {
return
}
var (
v400 = netip.PrefixFrom(netip.IPv4Unspecified(), 0)
v600000 = netip.PrefixFrom(netip.IPv6Unspecified(), 0)
v401 = netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 1)
v600001 = netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 1)
v41281 = netip.PrefixFrom(netip.AddrFrom4([4]byte{0x80}), 1)
v680001 = netip.PrefixFrom(netip.AddrFrom16([16]byte{0x80}), 1)
)
block := dlg.blockUntunneledTrafficCB.Checked()
cfg, err := conf.FromWgQuick(dlg.syntaxEdit.Text(), "temporary")
var newAllowedIPs []netip.Prefix
if err != nil {
goto err
}
if len(cfg.Peers) != 1 {
goto err
}
newAllowedIPs = make([]netip.Prefix, 0, len(cfg.Peers[0].AllowedIPs))
if block {
var (
foundV401 bool
foundV41281 bool
foundV600001 bool
foundV680001 bool
)
for _, allowedip := range cfg.Peers[0].AllowedIPs {
if allowedip == v600001 {
foundV600001 = true
} else if allowedip == v680001 {
foundV680001 = true
} else if allowedip == v401 {
foundV401 = true
} else if allowedip == v41281 {
foundV41281 = true
} else {
newAllowedIPs = append(newAllowedIPs, allowedip)
}
}
if !((foundV401 && foundV41281) || (foundV600001 && foundV680001)) {
goto err
}
if foundV401 && foundV41281 {
newAllowedIPs = append(newAllowedIPs, v400)
} else if foundV401 {
newAllowedIPs = append(newAllowedIPs, v401)
} else if foundV41281 {
newAllowedIPs = append(newAllowedIPs, v41281)
}
if foundV600001 && foundV680001 {
newAllowedIPs = append(newAllowedIPs, v600000)
} else if foundV600001 {
newAllowedIPs = append(newAllowedIPs, v600001)
} else if foundV680001 {
newAllowedIPs = append(newAllowedIPs, v680001)
}
cfg.Peers[0].AllowedIPs = newAllowedIPs
} else {
var (
foundV400 bool
foundV600000 bool
)
for _, allowedip := range cfg.Peers[0].AllowedIPs {
if allowedip == v600000 {
foundV600000 = true
} else if allowedip == v400 {
foundV400 = true
} else {
newAllowedIPs = append(newAllowedIPs, allowedip)
}
}
if !(foundV400 || foundV600000) {
goto err
}
if foundV400 {
newAllowedIPs = append(newAllowedIPs, v401)
newAllowedIPs = append(newAllowedIPs, v41281)
}
if foundV600000 {
newAllowedIPs = append(newAllowedIPs, v600001)
newAllowedIPs = append(newAllowedIPs, v680001)
}
cfg.Peers[0].AllowedIPs = newAllowedIPs
}
dlg.syntaxEdit.SetText(cfg.ToWgQuick())
return
err:
text := dlg.syntaxEdit.Text()
dlg.syntaxEdit.SetText("")
dlg.syntaxEdit.SetText(text)
}
func (dlg *EditDialog) onBlockUntunneledTrafficStateChanged(state int) {
dlg.blockUntunneledTraficCheckGuard = true
switch syntax.BlockState(state) {
case syntax.InevaluableBlockingUntunneledTraffic:
dlg.blockUntunneledTrafficCB.SetVisible(false)
case syntax.BlockingUntunneledTraffic:
dlg.blockUntunneledTrafficCB.SetVisible(true)
dlg.blockUntunneledTrafficCB.SetChecked(true)
case syntax.NotBlockingUntunneledTraffic:
dlg.blockUntunneledTrafficCB.SetVisible(true)
dlg.blockUntunneledTrafficCB.SetChecked(false)
}
dlg.blockUntunneledTraficCheckGuard = false
}
func (dlg *EditDialog) onSyntaxEditPrivateKeyChanged(privateKey string) {
if privateKey == dlg.lastPrivateKey {
return
}
dlg.lastPrivateKey = privateKey
key, _ := conf.NewPrivateKeyFromString(privateKey)
if key != nil {
dlg.pubkeyEdit.SetText(key.Public().String())
} else {
dlg.pubkeyEdit.SetText(l18n.Sprintf("(unknown)"))
}
}
func (dlg *EditDialog) onSaveButtonClicked() {
newName := dlg.nameEdit.Text()
if newName == "" {
showWarningCustom(dlg, l18n.Sprintf("Invalid name"), l18n.Sprintf("A name is required."))
return
}
if !conf.TunnelNameIsValid(newName) {
showWarningCustom(dlg, l18n.Sprintf("Invalid name"), l18n.Sprintf("Tunnel name %s is invalid.", newName))
return
}
newNameLower := strings.ToLower(newName)
if newNameLower != strings.ToLower(dlg.config.Name) {
existingTunnelList, err := manager.IPCClientTunnels()
if err != nil {
showWarningCustom(dlg, l18n.Sprintf("Unable to list existing tunnels"), err.Error())
return
}
for _, tunnel := range existingTunnelList {
if strings.ToLower(tunnel.Name) == newNameLower {
showWarningCustom(dlg, l18n.Sprintf("Tunnel already exists"), l18n.Sprintf("Another tunnel already exists with the name %s.", newName))
return
}
}
}
cfg, err := conf.FromWgQuick(dlg.syntaxEdit.Text(), newName)
if err != nil {
showErrorCustom(dlg, l18n.Sprintf("Unable to create new configuration"), err.Error())
return
}
dlg.config = *cfg
dlg.Accept()
}

View File

@@ -1,46 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"os"
"github.com/lxn/walk"
"github.com/amnezia-vpn/awg-windows/l18n"
)
func writeFileWithOverwriteHandling(owner walk.Form, filePath string, write func(file *os.File) error) bool {
showError := func(err error) bool {
if err == nil {
return false
}
showErrorCustom(owner, l18n.Sprintf("Writing file failed"), err.Error())
return true
}
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o600)
if err != nil {
if os.IsExist(err) {
if walk.DlgCmdNo == walk.MsgBox(owner, l18n.Sprintf("Writing file failed"), l18n.Sprintf(`File %s already exists.
Do you want to overwrite it?`, filePath), walk.MsgBoxYesNo|walk.MsgBoxDefButton2|walk.MsgBoxIconWarning) {
return false
}
if file, err = os.Create(filePath); err != nil {
return !showError(err)
}
} else {
return !showError(err)
}
}
defer file.Close()
return !showError(write(file))
}

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256" height="256" version="1.1" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><g><circle cx="128" cy="128" r="120" fill="#e1e1e1" stroke="#cacaca" stroke-linecap="square" stroke-width="8"/></g></svg>

Before

Width:  |  Height:  |  Size: 262 B

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="400" height="400" version="1.1" viewBox="0 0 400 400" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="a"><path d="m0 300h300v-300h-300z"/></clipPath></defs><g transform="matrix(1.3333 0 0 -1.3333 0 400)"><g clip-path="url(#a)"><g transform="translate(177.57 268.56)"><path d="m0 0c0.969-0.066 2.097 0.81 3.987 1.635-1.862 0.802-2.973 1.655-3.939 1.593-0.996-0.063-2.592-1.031-2.567-1.578 0.027-0.603 1.532-1.583 2.519-1.65" fill="#871719"/></g><g transform="translate(179.32 268.11)"><path d="m0 0c0.969-0.066 2.097 0.81 3.987 1.635-1.862 0.802-2.973 1.655-3.939 1.593-0.996-0.063-2.592-1.031-2.567-1.578 0.027-0.603 1.532-1.583 2.519-1.65" fill="#871719"/></g><g transform="translate(299.74 154.44)"><path d="m0 0s6.94 145.56-153.04 145.56c-141.48 0-145.9-139.63-145.9-139.63s-20.811-160.37 149.16-160.37c163.02 0 149.78 154.44 149.78 154.44" fill="#871719"/></g><g transform="translate(133.86 128.17)"><path d="m0 0c-2.627-1.39-4.65-2.414-6.63-3.517-8.1-4.512-15.026-10.419-20.544-17.868-1.784-2.409-3.01-2.603-5.727-0.941-35.338 21.61-37.609 75.843 0.983 99.453 30.017 18.364 68.365 7.14 82.735-20.477 2.723-5.234 3.069-13.291 1.345-18.782-5.955-18.955-20.015-29.586-39.313-34.102 5.689 4.87 10.218 10.393 11.659 18.025 1.452 7.687-0.084 14.638-4.542 20.956-6.773 9.596-19.868 13.544-30.811 9.389-11.881-4.511-18.39-15.354-17.216-28.683 1.09-12.381 10.484-20.405 28.061-23.453" fill="#fff"/></g><g transform="translate(58.513 66.293)"><path d="M 0,0 C 2.838,19.152 25.265,36.788 44.23,34.776 38.356,26.832 35.643,17.846 34.988,8.883 28.686,7.722 22.747,6.941 16.981,5.478 11.304,4.037 5.803,1.903 0,0" fill="#fff"/></g><g transform="translate(183.79 273.09)"><path d="m0 0c1.061 0.812 2.155 1.494 3.472 0.408 0.75-0.617 1.478-1.257 2.386-2.032-1.127-0.595-2.042-1.096-2.975-1.567-1.306-0.658-2.282-0.218-3.072 0.822-0.642 0.844-0.757 1.645 0.189 2.369m15.447-157.8c-1.598 1.382-2.611 1.381-4.485 0.182-6.359-4.068-12.867-7.922-19.481-11.562-3.792-2.086-7.898-3.599-12.653-5.724 1.633-0.421 2.418-0.619 3.201-0.827 17.776-4.73 27.272-20.335 23.065-37.813-3.741-15.544-19.52-25.482-34.812-22.86-12.748 2.186-23.877 12.772-25.735 25.456-2.026 13.824 4.859 27.119 17.108 32.689 6.794 3.089 13.771 5.778 20.549 8.9 7.706 3.551 16.038 6.355 22.766 11.296 16.7 12.262 27.012 29.145 31.033 49.523 2.408 12.207 2.245 24.36-3.339 35.95-4.286 8.895-11.319 15.357-18.875 21.253-7.775 6.068-16.007 11.554-23.747 17.664-2.095 1.653-3.509 4.505-4.478 7.09-0.411 1.095 0.925 4.066 1.819 4.227 4.746 0.852 9.596 1.29 14.425 1.473 5.574 0.21 11.164 0.032 16.746-0.042 1.21-0.015 2.853 0.141 3.549-0.542 2.891-2.843 5.159-1.014 7.166 0.856 1.689 1.573 2.893 3.668 4.236 5.433-0.815 0.12-2.487 0.541-4.168 0.581-5.613 0.133-11.233 0.047-16.843 0.253-1 0.037-1.963 1.066-2.942 1.637 1.031 0.409 2.058 1.165 3.093 1.175 9.682 0.091 19.366 0.054 29.057 0.054 0.011 5.038-6.722 11.936-12.704 13.806-0.045-0.682-0.087-1.317-0.131-1.994-5.944-0.141-11.778-0.03-17.078 2.788-1.396 0.743-2.309 2.394-3.446 3.627-1.431 1.551-2.605 3.547-4.349 4.559-3.576 2.076-7.48 3.58-11.211 5.397-13.259 6.458-27.262 6.231-42.302 4.854 8.991-2.092 17.11-3.982 25.23-5.872-0.093-0.494-0.185-0.987-0.278-1.481-10.86-1.455-21.134 2.528-31.756 4.003 3.849-2.254 7.749-4.35 11.778-6.158 4.095-1.837 8.316-3.39 12.538-5.091-5.364-4.583-10.746-5.588-17.488-4.048-3.686 0.842-7.585 1.29-11.348 1.106-3.887-0.19-7.802-1.147-11.332-3.506 3.78-1.916 7.263-3.506 10.549-5.432 1.355-0.795 2.909-2.144 3.287-3.536 0.904-3.333 1.166-6.841 1.687-10.281-6.188-0.701-17.071-6.994-19.27-11.09 9.512-1.831 19.868 0.383 28.942-5.746-2.989-2.262-9.949-5.075-12.502-7.007 3.156-0.827 10.469-0.423 13.33-0.229 2.409 0.164 3.521 0.223 4.508-0.59l28.001-21.921c2.944-2.374 14.835-13.629 17.939-20.704 2.643-6.023 2.966-11.148 2.965-12.398-2e-3 -3.355-0.413-8.609-2.721-14.469-0.969-2.461-3.812-7.912-9.677-14.267-9.09-9.847-20.783-15.17-33.57-17.807-29.732-6.13-54.436-37.881-47.462-72.884 8.142-40.866 53.247-62.991 90.107-43.552 23.824 12.564 36.456 37.078 33.072 63.762-2.045 16.12-9.338 29.269-21.563 39.839" fill="#fff"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,144 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"github.com/lxn/walk"
"github.com/amnezia-vpn/awg-windows/l18n"
"golang.zx2c4.com/wireguard/windows/manager"
)
type widthAndState struct {
width int
state manager.TunnelState
}
type widthAndDllIdx struct {
width int
idx int32
dll string
}
var cachedOverlayIconsForWidthAndState = make(map[widthAndState]walk.Image)
func iconWithOverlayForState(state manager.TunnelState, size int) (icon walk.Image, err error) {
icon = cachedOverlayIconsForWidthAndState[widthAndState{size, state}]
if icon != nil {
return
}
wireguardIcon, err := loadLogoIcon(size)
if err != nil {
return
}
if state == manager.TunnelStopped {
return wireguardIcon, err // TODO: if we find something prettier than the gray dot, then remove this clause
}
iconSize := wireguardIcon.Size()
w := int(float64(iconSize.Width) * 0.65)
h := int(float64(iconSize.Height) * 0.65)
overlayBounds := walk.Rectangle{iconSize.Width - w, iconSize.Height - h, w, h}
overlayIcon, err := iconForState(state, overlayBounds.Width)
if err != nil {
return
}
icon = walk.NewPaintFuncImage(walk.Size{size, size}, func(canvas *walk.Canvas, bounds walk.Rectangle) error {
if err := canvas.DrawImageStretched(wireguardIcon, bounds); err != nil {
return err
}
if err := canvas.DrawImageStretched(overlayIcon, overlayBounds); err != nil {
return err
}
return nil
})
cachedOverlayIconsForWidthAndState[widthAndState{size, state}] = icon
return
}
var cachedIconsForWidthAndState = make(map[widthAndState]*walk.Icon)
func iconForState(state manager.TunnelState, size int) (icon *walk.Icon, err error) {
icon = cachedIconsForWidthAndState[widthAndState{size, state}]
if icon != nil {
return
}
switch state {
case manager.TunnelStarted:
icon, err = loadSystemIcon("imageres", -106, size)
case manager.TunnelStopped:
icon, err = walk.NewIconFromResourceIdWithSize(8, walk.Size{size, size}) // TODO: replace with real icon from imageres/shell32
default:
icon, err = loadSystemIcon("shell32", -16739, size) // TODO: this doesn't look that great overlayed on the app icon
}
if err == nil {
cachedIconsForWidthAndState[widthAndState{size, state}] = icon
}
return
}
func textForState(state manager.TunnelState, withEllipsis bool) (text string) {
switch state {
case manager.TunnelStarted:
text = l18n.Sprintf("Active")
case manager.TunnelStarting:
text = l18n.Sprintf("Activating")
case manager.TunnelStopped:
text = l18n.Sprintf("Inactive")
case manager.TunnelStopping:
text = l18n.Sprintf("Deactivating")
case manager.TunnelUnknown:
text = l18n.Sprintf("Unknown state")
}
if withEllipsis {
switch state {
case manager.TunnelStarting, manager.TunnelStopping:
text += "…"
}
}
return
}
var cachedSystemIconsForWidthAndDllIdx = make(map[widthAndDllIdx]*walk.Icon)
func loadSystemIcon(dll string, index int32, size int) (icon *walk.Icon, err error) {
icon = cachedSystemIconsForWidthAndDllIdx[widthAndDllIdx{size, index, dll}]
if icon != nil {
return
}
icon, err = walk.NewIconFromSysDLLWithSize(dll, int(index), size)
if err == nil {
cachedSystemIconsForWidthAndDllIdx[widthAndDllIdx{size, index, dll}] = icon
}
return
}
func loadShieldIcon(size int) (icon *walk.Icon, err error) {
icon, err = loadSystemIcon("imageres", -1028, size)
if err != nil {
icon, err = loadSystemIcon("imageres", 1, size)
}
return
}
var cachedLogoIconsForWidth = make(map[int]*walk.Icon)
func loadLogoIcon(size int) (icon *walk.Icon, err error) {
icon = cachedLogoIconsForWidth[size]
if icon != nil {
return
}
icon, err = walk.NewIconFromResourceIdWithSize(7, walk.Size{size, size})
if err == nil {
cachedLogoIconsForWidth[size] = icon
}
return
}

View File

@@ -1,290 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"sort"
"sync/atomic"
"github.com/lxn/win"
"github.com/amnezia-vpn/awg-windows/conf"
"golang.zx2c4.com/wireguard/windows/manager"
"github.com/lxn/walk"
)
// ListModel is a struct to store the currently known tunnels to the GUI, suitable as a model for a walk.TableView.
type ListModel struct {
walk.TableModelBase
walk.SorterBase
tunnels []manager.Tunnel
lastObservedState map[manager.Tunnel]manager.TunnelState
}
var cachedListViewIconsForWidthAndState = make(map[widthAndState]*walk.Bitmap)
func (t *ListModel) RowCount() int {
return len(t.tunnels)
}
func (t *ListModel) Value(row, col int) any {
if col != 0 || row < 0 || row >= len(t.tunnels) {
return ""
}
return t.tunnels[row].Name
}
func (t *ListModel) Sort(col int, order walk.SortOrder) error {
sort.SliceStable(t.tunnels, func(i, j int) bool {
return conf.TunnelNameIsLess(t.tunnels[i].Name, t.tunnels[j].Name)
})
return t.SorterBase.Sort(col, order)
}
type ListView struct {
*walk.TableView
model *ListModel
tunnelChangedCB *manager.TunnelChangeCallback
tunnelsChangedCB *manager.TunnelsChangeCallback
tunnelsUpdateSuspended int32
}
func NewListView(parent walk.Container) (*ListView, error) {
var disposables walk.Disposables
defer disposables.Treat()
tv, err := walk.NewTableView(parent)
if err != nil {
return nil, err
}
disposables.Add(tv)
tv.SetDoubleBuffering(true)
model := new(ListModel)
model.lastObservedState = make(map[manager.Tunnel]manager.TunnelState)
tv.SetModel(model)
tv.SetLastColumnStretched(true)
tv.SetHeaderHidden(true)
tv.SetIgnoreNowhere(true)
tv.SetScrollbarOrientation(walk.Vertical)
tv.Columns().Add(walk.NewTableViewColumn())
tunnelsView := &ListView{
TableView: tv,
model: model,
}
tv.SetCellStyler(tunnelsView)
disposables.Spare()
tunnelsView.tunnelChangedCB = manager.IPCClientRegisterTunnelChange(tunnelsView.onTunnelChange)
tunnelsView.tunnelsChangedCB = manager.IPCClientRegisterTunnelsChange(tunnelsView.onTunnelsChange)
return tunnelsView, nil
}
func (tv *ListView) Dispose() {
if tv.tunnelChangedCB != nil {
tv.tunnelChangedCB.Unregister()
tv.tunnelChangedCB = nil
}
if tv.tunnelsChangedCB != nil {
tv.tunnelsChangedCB.Unregister()
tv.tunnelsChangedCB = nil
}
tv.TableView.Dispose()
}
func (tv *ListView) CurrentTunnel() *manager.Tunnel {
idx := tv.CurrentIndex()
if idx == -1 {
return nil
}
return &tv.model.tunnels[idx]
}
var dummyBitmap *walk.Bitmap
func (tv *ListView) StyleCell(style *walk.CellStyle) {
row := style.Row()
if row < 0 || row >= len(tv.model.tunnels) {
return
}
tunnel := &tv.model.tunnels[row]
var state manager.TunnelState
var ok bool
state, ok = tv.model.lastObservedState[tv.model.tunnels[row]]
if !ok {
var err error
state, err = tunnel.State()
if err != nil {
return
}
tv.model.lastObservedState[tv.model.tunnels[row]] = state
}
icon, err := iconForState(state, 14)
if err != nil {
return
}
margin := tv.IntFrom96DPI(1)
bitmapWidth := tv.IntFrom96DPI(16)
if win.IsAppThemed() {
cacheKey := widthAndState{bitmapWidth, state}
if cacheValue, ok := cachedListViewIconsForWidthAndState[cacheKey]; ok {
style.Image = cacheValue
return
}
bitmap, err := walk.NewBitmapWithTransparentPixelsForDPI(walk.Size{bitmapWidth, bitmapWidth}, tv.DPI())
if err != nil {
return
}
canvas, err := walk.NewCanvasFromImage(bitmap)
if err != nil {
return
}
bounds := walk.Rectangle{X: margin, Y: margin, Height: bitmapWidth - 2*margin, Width: bitmapWidth - 2*margin}
err = canvas.DrawImageStretchedPixels(icon, bounds)
canvas.Dispose()
if err != nil {
return
}
cachedListViewIconsForWidthAndState[cacheKey] = bitmap
style.Image = bitmap
} else {
if dummyBitmap == nil {
dummyBitmap, _ = walk.NewBitmapForDPI(tv.SizeFrom96DPI(walk.Size{}), 96)
}
style.Image = dummyBitmap
canvas := style.Canvas()
if canvas == nil {
return
}
bounds := style.BoundsPixels()
bounds.Width = bitmapWidth - 2*margin
bounds.X = (bounds.Height - bounds.Width) / 2
bounds.Height = bounds.Width
bounds.Y += bounds.X
canvas.DrawImageStretchedPixels(icon, bounds)
}
}
func (tv *ListView) onTunnelChange(tunnel *manager.Tunnel, state, globalState manager.TunnelState, err error) {
tv.Synchronize(func() {
idx := -1
for i := range tv.model.tunnels {
if tv.model.tunnels[i].Name == tunnel.Name {
idx = i
break
}
}
if idx != -1 {
tv.model.lastObservedState[tv.model.tunnels[idx]] = state
tv.model.PublishRowChanged(idx)
return
}
})
}
func (tv *ListView) onTunnelsChange() {
if atomic.LoadInt32(&tv.tunnelsUpdateSuspended) == 0 {
tv.Load(true)
}
}
func (tv *ListView) SetSuspendTunnelsUpdate(suspend bool) {
if suspend {
atomic.AddInt32(&tv.tunnelsUpdateSuspended, 1)
} else {
atomic.AddInt32(&tv.tunnelsUpdateSuspended, -1)
}
tv.Load(true)
}
func (tv *ListView) Load(asyncUI bool) {
tunnels, err := manager.IPCClientTunnels()
if err != nil {
return
}
doUI := func() {
newTunnels := make(map[manager.Tunnel]bool, len(tunnels))
oldTunnels := make(map[manager.Tunnel]bool, len(tv.model.tunnels))
for _, tunnel := range tunnels {
newTunnels[tunnel] = true
}
for i := len(tv.model.tunnels); i > 0; {
i--
tunnel := tv.model.tunnels[i]
oldTunnels[tunnel] = true
if !newTunnels[tunnel] {
tv.model.tunnels = append(tv.model.tunnels[:i], tv.model.tunnels[i+1:]...)
tv.model.PublishRowsRemoved(i, i) // TODO: Do we have to call that everytime or can we pass a range?
delete(tv.model.lastObservedState, tunnel)
}
}
didAdd := false
firstTunnelName := ""
for tunnel := range newTunnels {
if !oldTunnels[tunnel] {
if len(firstTunnelName) == 0 || !conf.TunnelNameIsLess(firstTunnelName, tunnel.Name) {
firstTunnelName = tunnel.Name
}
tv.model.tunnels = append(tv.model.tunnels, tunnel)
didAdd = true
}
}
if didAdd {
tv.model.PublishRowsReset()
tv.model.Sort(tv.model.SortedColumn(), tv.model.SortOrder())
if len(tv.SelectedIndexes()) == 0 {
tv.selectTunnel(firstTunnelName)
}
}
}
if asyncUI {
tv.Synchronize(doUI)
} else {
doUI()
}
}
func (tv *ListView) selectTunnel(tunnelName string) {
for i, tunnel := range tv.model.tunnels {
if tunnel.Name == tunnelName {
tv.SetCurrentIndex(i)
break
}
}
}
func (tv *ListView) SelectFirstActiveTunnel() {
tunnels := make([]manager.Tunnel, len(tv.model.tunnels))
copy(tunnels, tv.model.tunnels)
go func() {
for _, tunnel := range tunnels {
state, err := tunnel.State()
if err != nil {
continue
}
if state == manager.TunnelStarting || state == manager.TunnelStarted {
tv.Synchronize(func() {
tv.selectTunnel(tunnel.Name)
})
return
}
}
}()
}

View File

@@ -1,221 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"fmt"
"os"
"strings"
"time"
"github.com/amnezia-vpn/awg-windows/l18n"
"github.com/amnezia-vpn/awg-windows/ringlogger"
"github.com/lxn/walk"
)
const (
maxLogLinesDisplayed = 10000
)
type LogPage struct {
*walk.TabPage
logView *walk.TableView
model *logModel
}
func NewLogPage() (*LogPage, error) {
lp := &LogPage{}
var err error
var disposables walk.Disposables
defer disposables.Treat()
if lp.TabPage, err = walk.NewTabPage(); err != nil {
return nil, err
}
disposables.Add(lp)
lp.Disposing().Attach(func() {
lp.model.quit <- true
})
lp.SetTitle(l18n.Sprintf("Log"))
lp.SetLayout(walk.NewVBoxLayout())
if lp.logView, err = walk.NewTableView(lp); err != nil {
return nil, err
}
lp.logView.SetAlternatingRowBG(true)
lp.logView.SetLastColumnStretched(true)
lp.logView.SetGridlines(true)
contextMenu, err := walk.NewMenu()
if err != nil {
return nil, err
}
lp.logView.AddDisposable(contextMenu)
copyAction := walk.NewAction()
copyAction.SetText(l18n.Sprintf("&Copy"))
copyAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyC})
copyAction.Triggered().Attach(lp.onCopy)
contextMenu.Actions().Add(copyAction)
lp.ShortcutActions().Add(copyAction)
selectAllAction := walk.NewAction()
selectAllAction.SetText(l18n.Sprintf("Select &all"))
selectAllAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyA})
selectAllAction.Triggered().Attach(lp.onSelectAll)
contextMenu.Actions().Add(selectAllAction)
lp.ShortcutActions().Add(selectAllAction)
saveAction := walk.NewAction()
saveAction.SetText(l18n.Sprintf("&Save to file…"))
saveAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyS})
saveAction.Triggered().Attach(lp.onSave)
contextMenu.Actions().Add(saveAction)
lp.ShortcutActions().Add(saveAction)
lp.logView.SetContextMenu(contextMenu)
setSelectionStatus := func() {
copyAction.SetEnabled(len(lp.logView.SelectedIndexes()) > 0)
selectAllAction.SetEnabled(len(lp.logView.SelectedIndexes()) < len(lp.model.items))
}
lp.logView.SelectedIndexesChanged().Attach(setSelectionStatus)
stampCol := walk.NewTableViewColumn()
stampCol.SetName("Stamp")
stampCol.SetTitle(l18n.Sprintf("Time"))
stampCol.SetFormat("2006-01-02 15:04:05.000")
stampCol.SetWidth(140)
lp.logView.Columns().Add(stampCol)
msgCol := walk.NewTableViewColumn()
msgCol.SetName("Line")
msgCol.SetTitle(l18n.Sprintf("Log message"))
lp.logView.Columns().Add(msgCol)
lp.model = newLogModel(lp)
lp.model.RowsReset().Attach(setSelectionStatus)
lp.logView.SetModel(lp.model)
setSelectionStatus()
buttonsContainer, err := walk.NewComposite(lp)
if err != nil {
return nil, err
}
buttonsContainer.SetLayout(walk.NewHBoxLayout())
buttonsContainer.Layout().SetMargins(walk.Margins{})
walk.NewHSpacer(buttonsContainer)
saveButton, err := walk.NewPushButton(buttonsContainer)
if err != nil {
return nil, err
}
saveButton.SetText(l18n.Sprintf("&Save"))
saveButton.Clicked().Attach(lp.onSave)
disposables.Spare()
return lp, nil
}
func (lp *LogPage) isAtBottom() bool {
return len(lp.model.items) == 0 || lp.logView.ItemVisible(len(lp.model.items)-1)
}
func (lp *LogPage) scrollToBottom() {
lp.logView.EnsureItemVisible(len(lp.model.items) - 1)
}
func (lp *LogPage) onCopy() {
var logLines strings.Builder
selectedItemIndexes := lp.logView.SelectedIndexes()
if len(selectedItemIndexes) == 0 {
return
}
for i := 0; i < len(selectedItemIndexes); i++ {
logItem := lp.model.items[selectedItemIndexes[i]]
logLines.WriteString(fmt.Sprintf("%s: %s\r\n", logItem.Stamp.Format("2006-01-02 15:04:05.000"), logItem.Line))
}
walk.Clipboard().SetText(logLines.String())
}
func (lp *LogPage) onSelectAll() {
lp.logView.SetSelectedIndexes([]int{-1})
}
func (lp *LogPage) onSave() {
fd := walk.FileDialog{
Filter: l18n.Sprintf("Text Files (*.txt)|*.txt|All Files (*.*)|*.*"),
FilePath: fmt.Sprintf("wireguard-log-%s.txt", time.Now().Format("2006-01-02T150405")),
Title: l18n.Sprintf("Export log to file"),
}
form := lp.Form()
if ok, _ := fd.ShowSave(form); !ok {
return
}
if fd.FilterIndex == 1 && !strings.HasSuffix(fd.FilePath, ".txt") {
fd.FilePath = fd.FilePath + ".txt"
}
writeFileWithOverwriteHandling(form, fd.FilePath, func(file *os.File) error {
if _, err := ringlogger.Global.WriteTo(file); err != nil {
return fmt.Errorf("exportLog: Ringlogger.WriteTo failed: %w", err)
}
return nil
})
}
type logModel struct {
walk.ReflectTableModelBase
lp *LogPage
quit chan bool
items []ringlogger.FollowLine
}
func newLogModel(lp *LogPage) *logModel {
mdl := &logModel{lp: lp, quit: make(chan bool)}
go func() {
ticker := time.NewTicker(time.Second)
cursor := ringlogger.CursorAll
for {
select {
case <-ticker.C:
var items []ringlogger.FollowLine
items, cursor = ringlogger.Global.FollowFromCursor(cursor)
if len(items) == 0 {
continue
}
mdl.lp.Synchronize(func() {
isAtBottom := mdl.lp.isAtBottom() && len(lp.logView.SelectedIndexes()) <= 1
mdl.items = append(mdl.items, items...)
if len(mdl.items) > maxLogLinesDisplayed {
mdl.items = mdl.items[len(mdl.items)-maxLogLinesDisplayed:]
}
mdl.PublishRowsReset()
if isAtBottom {
mdl.lp.scrollToBottom()
}
})
case <-mdl.quit:
ticker.Stop()
break
}
}
}()
return mdl
}
func (mdl *logModel) Items() any {
return mdl.items
}

View File

@@ -1,239 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"sync"
"unsafe"
"github.com/lxn/walk"
"github.com/lxn/win"
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/awg-windows/l18n"
"golang.zx2c4.com/wireguard/windows/manager"
)
type ManageTunnelsWindow struct {
walk.FormBase
tabs *walk.TabWidget
tunnelsPage *TunnelsPage
logPage *LogPage
updatePage *UpdatePage
tunnelChangedCB *manager.TunnelChangeCallback
}
const (
manageWindowWindowClass = "WireGuard UI - Manage Tunnels"
raiseMsg = win.WM_USER + 0x3510
aboutWireGuardCmd = 0x37
)
var taskbarButtonCreatedMsg uint32
var initedManageTunnels sync.Once
func NewManageTunnelsWindow() (*ManageTunnelsWindow, error) {
initedManageTunnels.Do(func() {
walk.AppendToWalkInit(func() {
walk.MustRegisterWindowClass(manageWindowWindowClass)
taskbarButtonCreatedMsg = win.RegisterWindowMessage(windows.StringToUTF16Ptr("TaskbarButtonCreated"))
})
})
var err error
var disposables walk.Disposables
defer disposables.Treat()
font, err := walk.NewFont("Segoe UI", 9, 0)
if err != nil {
return nil, err
}
mtw := new(ManageTunnelsWindow)
mtw.SetName("WireGuard")
err = walk.InitWindow(mtw, nil, manageWindowWindowClass, win.WS_OVERLAPPEDWINDOW, win.WS_EX_CONTROLPARENT)
if err != nil {
return nil, err
}
disposables.Add(mtw)
win.ChangeWindowMessageFilterEx(mtw.Handle(), raiseMsg, win.MSGFLT_ALLOW, nil)
mtw.SetPersistent(true)
if icon, err := loadLogoIcon(32); err == nil {
mtw.SetIcon(icon)
}
mtw.SetTitle("WireGuard")
mtw.SetFont(font)
mtw.SetSize(walk.Size{675, 525})
mtw.SetMinMaxSize(walk.Size{500, 400}, walk.Size{0, 0})
vlayout := walk.NewVBoxLayout()
vlayout.SetMargins(walk.Margins{5, 5, 5, 5})
mtw.SetLayout(vlayout)
mtw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) {
// "Close to tray" instead of exiting application
*canceled = true
if !noTrayAvailable {
mtw.Hide()
} else {
win.ShowWindow(mtw.Handle(), win.SW_MINIMIZE)
}
})
mtw.VisibleChanged().Attach(func() {
if mtw.Visible() {
mtw.tunnelsPage.updateConfView()
win.SetForegroundWindow(mtw.Handle())
win.BringWindowToTop(mtw.Handle())
mtw.logPage.scrollToBottom()
}
})
if mtw.tabs, err = walk.NewTabWidget(mtw); err != nil {
return nil, err
}
if mtw.tunnelsPage, err = NewTunnelsPage(); err != nil {
return nil, err
}
mtw.tabs.Pages().Add(mtw.tunnelsPage.TabPage)
mtw.tunnelsPage.CreateToolbar()
if mtw.logPage, err = NewLogPage(); err != nil {
return nil, err
}
mtw.tabs.Pages().Add(mtw.logPage.TabPage)
mtw.tunnelChangedCB = manager.IPCClientRegisterTunnelChange(mtw.onTunnelChange)
globalState, _ := manager.IPCClientGlobalState()
mtw.onTunnelChange(nil, manager.TunnelUnknown, globalState, nil)
systemMenu := win.GetSystemMenu(mtw.Handle(), false)
if systemMenu != 0 {
win.InsertMenuItem(systemMenu, 0, true, &win.MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})),
FMask: win.MIIM_ID | win.MIIM_STRING | win.MIIM_FTYPE,
FType: win.MIIM_STRING,
DwTypeData: windows.StringToUTF16Ptr(l18n.Sprintf("&About WireGuard…")),
WID: uint32(aboutWireGuardCmd),
})
win.InsertMenuItem(systemMenu, 1, true, &win.MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})),
FMask: win.MIIM_TYPE,
FType: win.MFT_SEPARATOR,
})
}
disposables.Spare()
return mtw, nil
}
func (mtw *ManageTunnelsWindow) Dispose() {
if mtw.tunnelChangedCB != nil {
mtw.tunnelChangedCB.Unregister()
mtw.tunnelChangedCB = nil
}
mtw.FormBase.Dispose()
}
func (mtw *ManageTunnelsWindow) updateProgressIndicator(globalState manager.TunnelState) {
pi := mtw.ProgressIndicator()
if pi == nil {
return
}
switch globalState {
case manager.TunnelStopping, manager.TunnelStarting:
pi.SetState(walk.PIIndeterminate)
default:
pi.SetState(walk.PINoProgress)
}
if icon, err := iconForState(globalState, 16); err == nil {
if globalState == manager.TunnelStopped {
icon = nil
}
pi.SetOverlayIcon(icon, textForState(globalState, false))
}
}
func (mtw *ManageTunnelsWindow) onTunnelChange(tunnel *manager.Tunnel, state, globalState manager.TunnelState, err error) {
mtw.Synchronize(func() {
mtw.updateProgressIndicator(globalState)
if err != nil && mtw.Visible() {
errMsg := err.Error()
if len(errMsg) > 0 && errMsg[len(errMsg)-1] != '.' {
errMsg += "."
}
showWarningCustom(mtw, l18n.Sprintf("Tunnel Error"), l18n.Sprintf("%s\n\nPlease consult the log for more information.", errMsg))
}
})
}
func (mtw *ManageTunnelsWindow) UpdateFound() {
if mtw.updatePage != nil {
return
}
if IsAdmin {
mtw.SetTitle(l18n.Sprintf("%s (out of date)", mtw.Title()))
}
updatePage, err := NewUpdatePage()
if err == nil {
mtw.updatePage = updatePage
mtw.tabs.Pages().Add(updatePage.TabPage)
}
}
func (mtw *ManageTunnelsWindow) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case win.WM_QUERYENDSESSION:
if lParam == win.ENDSESSION_CLOSEAPP {
return win.TRUE
}
case win.WM_ENDSESSION:
if lParam == win.ENDSESSION_CLOSEAPP && wParam == 1 {
walk.App().Exit(198)
}
case win.WM_SYSCOMMAND:
if wParam == aboutWireGuardCmd {
onAbout(mtw)
return 0
}
case raiseMsg:
if mtw.tunnelsPage == nil || mtw.tabs == nil {
mtw.Synchronize(func() {
mtw.SendMessage(msg, wParam, lParam)
})
return 0
}
if !mtw.Visible() {
mtw.tunnelsPage.listView.SelectFirstActiveTunnel()
if mtw.tabs.Pages().Len() != 3 {
mtw.tabs.SetCurrentIndex(0)
}
}
if mtw.tabs.Pages().Len() == 3 {
mtw.tabs.SetCurrentIndex(2)
}
raise(mtw.Handle())
return 0
case taskbarButtonCreatedMsg:
ret := mtw.FormBase.WndProc(hwnd, msg, wParam, lParam)
go func() {
globalState, err := manager.IPCClientGlobalState()
if err == nil {
mtw.Synchronize(func() {
mtw.updateProgressIndicator(globalState)
})
}
}()
return ret
}
return mtw.FormBase.WndProc(hwnd, msg, wParam, lParam)
}

View File

@@ -1,79 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"os"
"runtime"
"github.com/lxn/win"
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/awg-windows/l18n"
)
func raise(hwnd win.HWND) {
if win.IsIconic(hwnd) {
win.ShowWindow(hwnd, win.SW_RESTORE)
}
win.SetActiveWindow(hwnd)
win.SetWindowPos(hwnd, win.HWND_TOPMOST, 0, 0, 0, 0, win.SWP_NOMOVE|win.SWP_NOSIZE|win.SWP_SHOWWINDOW)
win.SetForegroundWindow(hwnd)
win.SetWindowPos(hwnd, win.HWND_NOTOPMOST, 0, 0, 0, 0, win.SWP_NOMOVE|win.SWP_NOSIZE|win.SWP_SHOWWINDOW)
}
func raiseRemote(hwnd win.HWND) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
win.SendMessage(hwnd, raiseMsg, 0, 0)
currentForegroundHwnd := win.GetForegroundWindow()
currentForegroundThreadId := win.GetWindowThreadProcessId(currentForegroundHwnd, nil)
currentThreadId := win.GetCurrentThreadId()
win.AttachThreadInput(int32(currentForegroundThreadId), int32(currentThreadId), true)
win.SetWindowPos(hwnd, win.HWND_TOPMOST, 0, 0, 0, 0, win.SWP_NOMOVE|win.SWP_NOSIZE|win.SWP_SHOWWINDOW)
win.SetWindowPos(hwnd, win.HWND_NOTOPMOST, 0, 0, 0, 0, win.SWP_NOMOVE|win.SWP_NOSIZE|win.SWP_SHOWWINDOW)
win.SetForegroundWindow(hwnd)
win.AttachThreadInput(int32(currentForegroundThreadId), int32(currentThreadId), false)
win.SetFocus(hwnd)
win.SetActiveWindow(hwnd)
}
func RaiseUI() bool {
hwnd := win.FindWindow(windows.StringToUTF16Ptr(manageWindowWindowClass), nil)
if hwnd == 0 {
return false
}
raiseRemote(hwnd)
return true
}
func WaitForRaiseUIThenQuit() {
var handle win.HWINEVENTHOOK
runtime.LockOSThread()
defer runtime.UnlockOSThread()
handle, err := win.SetWinEventHook(win.EVENT_OBJECT_CREATE, win.EVENT_OBJECT_CREATE, 0, func(hWinEventHook win.HWINEVENTHOOK, event uint32, hwnd win.HWND, idObject, idChild int32, idEventThread, dwmsEventTime uint32) uintptr {
class := make([]uint16, len(manageWindowWindowClass)+2) /* Plus 2, one for the null terminator, and one to see if this is only a prefix */
n, err := win.GetClassName(hwnd, &class[0], len(class))
if err != nil || n != len(manageWindowWindowClass) || windows.UTF16ToString(class) != manageWindowWindowClass {
return 0
}
win.UnhookWinEvent(handle)
raiseRemote(hwnd)
os.Exit(0)
return 0
}, 0, 0, win.WINEVENT_SKIPOWNPROCESS|win.WINEVENT_OUTOFCONTEXT)
if err != nil {
showErrorCustom(nil, l18n.Sprintf("WireGuard Detection Error"), l18n.Sprintf("Unable to wait for WireGuard window to appear: %v", err))
}
for {
var msg win.MSG
if m := win.GetMessage(&msg, 0, 0, 0); m != 0 {
win.TranslateMessage(&msg)
win.DispatchMessage(&msg)
}
}
}

View File

@@ -1,696 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*
* This is a direct translation of the original C, and for that reason, it's pretty unusual Go code:
* https://git.zx2c4.com/wireguard-tools/tree/contrib/highlighter/highlighter.c
*/
package syntax
import (
"math"
"unsafe"
)
type highlight int
const (
highlightSection highlight = iota
highlightField
highlightPrivateKey
highlightPublicKey
highlightPresharedKey
highlightIP
highlightCidr
highlightHost
highlightPort
highlightMTU
highlightKeepalive
highlightComment
highlightDelimiter
highlightTable
highlightCmd
highlightJc
highlightJmin
highlightJmax
highlightS1
highlightS2
highlightH1
highlightH2
highlightH3
highlightH4
highlightError
)
func validateHighlight(isValid bool, t highlight) highlight {
if isValid {
return t
}
return highlightError
}
type highlightSpan struct {
t highlight
s int
len int
}
func isDecimal(c byte) bool {
return c >= '0' && c <= '9'
}
func isHexadecimal(c byte) bool {
return isDecimal(c) || (c|32) >= 'a' && (c|32) <= 'f'
}
func isAlphabet(c byte) bool {
return (c|32) >= 'a' && (c|32) <= 'z'
}
type stringSpan struct {
s *byte
len int
}
func (s stringSpan) at(i int) *byte {
return (*byte)(unsafe.Add(unsafe.Pointer(s.s), uintptr(i)))
}
func (s stringSpan) isSame(c string) bool {
if s.len != len(c) {
return false
}
cb := ([]byte)(c)
for i := 0; i < s.len; i++ {
if *s.at(i) != cb[i] {
return false
}
}
return true
}
func (s stringSpan) isCaselessSame(c string) bool {
if s.len != len(c) {
return false
}
cb := ([]byte)(c)
for i := 0; i < s.len; i++ {
a := *s.at(i)
b := cb[i]
if a-'a' < 26 {
a &= 95
}
if b-'a' < 26 {
b &= 95
}
if a != b {
return false
}
}
return true
}
func (s stringSpan) isValidKey() bool {
if s.len != 44 || *s.at(43) != '=' {
return false
}
for i := 0; i < 42; i++ {
if !isDecimal(*s.at(i)) && !isAlphabet(*s.at(i)) && *s.at(i) != '/' && *s.at(i) != '+' {
return false
}
}
switch *s.at(42) {
case 'A', 'E', 'I', 'M', 'Q', 'U', 'Y', 'c', 'g', 'k', 'o', 's', 'w', '4', '8', '0':
return true
}
return false
}
func (s stringSpan) isValidHostname() bool {
numDigit := 0
numEntity := s.len
if s.len > 63 || s.len == 0 {
return false
}
if *s.s == '-' || *s.at(s.len - 1) == '-' {
return false
}
if *s.s == '.' || *s.at(s.len - 1) == '.' {
return false
}
for i := 0; i < s.len; i++ {
if isDecimal(*s.at(i)) {
numDigit++
continue
}
if *s.at(i) == '.' {
numEntity--
continue
}
if !isAlphabet(*s.at(i)) && *s.at(i) != '-' {
return false
}
if i != 0 && *s.at(i) == '.' && *s.at(i - 1) == '.' {
return false
}
}
return numDigit != numEntity
}
func (s stringSpan) isValidIPv4() bool {
pos := 0
for i := 0; i < 4 && pos < s.len; i++ {
val := 0
j := 0
for ; j < 3 && pos+j < s.len && isDecimal(*s.at(pos + j)); j++ {
val = 10*val + int(*s.at(pos + j)-'0')
}
if j == 0 || j > 1 && *s.at(pos) == '0' || val > 255 {
return false
}
if pos+j == s.len && i == 3 {
return true
}
if *s.at(pos + j) != '.' {
return false
}
pos += j + 1
}
return false
}
func (s stringSpan) isValidIPv6() bool {
if s.len < 2 {
return false
}
pos := 0
if *s.at(0) == ':' {
if *s.at(1) != ':' {
return false
}
pos = 1
}
if *s.at(s.len - 1) == ':' && *s.at(s.len - 2) != ':' {
return false
}
seenColon := false
for i := 0; pos < s.len; i++ {
if *s.at(pos) == ':' && !seenColon {
seenColon = true
pos++
if pos == s.len {
break
}
if i == 7 {
return false
}
continue
}
j := 0
for ; ; j++ {
if j < 4 && pos+j < s.len && isHexadecimal(*s.at(pos + j)) {
continue
}
break
}
if j == 0 {
return false
}
if pos+j == s.len && (seenColon || i == 7) {
break
}
if i == 7 {
return false
}
if *s.at(pos + j) != ':' {
if *s.at(pos + j) != '.' || i < 6 && !seenColon {
return false
}
return stringSpan{s.at(pos), s.len - pos}.isValidIPv4()
}
pos += j + 1
}
return true
}
func (s stringSpan) isValidUint(supportHex bool, min, max uint64) bool {
// Bound this around 32 bits, so that we don't have to write overflow logic.
if s.len > 10 || s.len == 0 {
return false
}
val := uint64(0)
if supportHex && s.len > 2 && *s.s == '0' && *s.at(1) == 'x' {
for i := 2; i < s.len; i++ {
if *s.at(i)-'0' < 10 {
val = 16*val + uint64(*s.at(i)-'0')
} else if (*s.at(i))|32-'a' < 6 {
val = 16*val + uint64((*s.at(i)|32)-'a'+10)
} else {
return false
}
}
} else {
for i := 0; i < s.len; i++ {
if !isDecimal(*s.at(i)) {
return false
}
val = 10*val + uint64(*s.at(i)-'0')
}
}
return val <= max && val >= min
}
func (s stringSpan) isValidPort() bool {
return s.isValidUint(false, 0, 65535)
}
func (s stringSpan) isValidUint16() bool {
return s.isValidUint(false, 0, math.MaxUint16)
}
func (s stringSpan) isValidUint32() bool {
return s.isValidUint(false, 0, math.MaxUint32)
}
func (s stringSpan) isValidMTU() bool {
return s.isValidUint(false, 576, 65535)
}
func (s stringSpan) isValidTable() bool {
return s.isSame("off") || s.isSame("auto") || s.isSame("main") || s.isValidUint(false, 0, (1<<32)-1)
}
func (s stringSpan) isValidPersistentKeepAlive() bool {
if s.isSame("off") {
return true
}
return s.isValidUint(false, 0, 65535)
}
// It's probably not worthwhile to try to validate a bash expression. So instead we just demand non-zero length.
func (s stringSpan) isValidPrePostUpDown() bool {
return s.len != 0
}
func (s stringSpan) isValidScope() bool {
if s.len > 64 || s.len == 0 {
return false
}
for i := 0; i < s.len; i++ {
if isAlphabet(*s.at(i)) && !isDecimal(*s.at(i)) && *s.at(i) != '_' && *s.at(i) != '=' && *s.at(i) != '+' && *s.at(i) != '.' && *s.at(i) != '-' {
return false
}
}
return true
}
func (s stringSpan) isValidEndpoint() bool {
if s.len == 0 {
return false
}
if *s.s == '[' {
seenScope := false
hostspan := stringSpan{s.at(1), 0}
for i := 1; i < s.len; i++ {
if *s.at(i) == '%' {
if seenScope {
return false
}
seenScope = true
if !hostspan.isValidIPv6() {
return false
}
hostspan = stringSpan{s.at(i + 1), 0}
} else if *s.at(i) == ']' {
if seenScope {
if !hostspan.isValidScope() {
return false
}
} else if !hostspan.isValidIPv6() {
return false
}
if i == s.len-1 || *s.at((i + 1)) != ':' {
return false
}
return stringSpan{s.at(i + 2), s.len - i - 2}.isValidPort()
} else {
hostspan.len++
}
}
return false
}
for i := 0; i < s.len; i++ {
if *s.at(i) == ':' {
host := stringSpan{s.s, i}
port := stringSpan{s.at(i + 1), s.len - i - 1}
return port.isValidPort() && (host.isValidIPv4() || host.isValidHostname())
}
}
return false
}
func (s stringSpan) isValidNetwork() bool {
for i := 0; i < s.len; i++ {
if *s.at(i) == '/' {
ip := stringSpan{s.s, i}
cidr := stringSpan{s.at(i + 1), s.len - i - 1}
cidrval := uint16(0)
if cidr.len > 3 || cidr.len == 0 {
return false
}
for j := 0; j < cidr.len; j++ {
if !isDecimal(*cidr.at(j)) {
return false
}
cidrval = 10*cidrval + uint16(*cidr.at(j)-'0')
}
if ip.isValidIPv4() {
return cidrval <= 32
} else if ip.isValidIPv6() {
return cidrval <= 128
}
return false
}
}
return s.isValidIPv4() || s.isValidIPv6()
}
type field int32
const (
fieldInterfaceSection field = iota
fieldPrivateKey
fieldListenPort
fieldAddress
fieldDNS
fieldMTU
fieldTable
fieldPreUp
fieldPostUp
fieldPreDown
fieldPostDown
fieldPeerSection
fieldPublicKey
fieldPresharedKey
fieldAllowedIPs
fieldEndpoint
fieldPersistentKeepalive
fieldJc
fieldJmin
fieldJmax
fieldS1
fieldS2
fieldH1
fieldH2
fieldH3
fieldH4
fieldInvalid
)
func sectionForField(t field) field {
if t > fieldInterfaceSection && t < fieldPeerSection {
return fieldInterfaceSection
}
if t > fieldPeerSection && t < fieldInvalid {
return fieldPeerSection
}
return fieldInvalid
}
func (s stringSpan) field() field {
switch {
case s.isCaselessSame("PrivateKey"):
return fieldPrivateKey
case s.isCaselessSame("ListenPort"):
return fieldListenPort
case s.isCaselessSame("Jc"):
return fieldJc
case s.isCaselessSame("Jmin"):
return fieldJmin
case s.isCaselessSame("Jmax"):
return fieldJmax
case s.isCaselessSame("S1"):
return fieldS1
case s.isCaselessSame("S2"):
return fieldS2
case s.isCaselessSame("H1"):
return fieldH1
case s.isCaselessSame("H2"):
return fieldH2
case s.isCaselessSame("H3"):
return fieldH3
case s.isCaselessSame("H4"):
return fieldH4
case s.isCaselessSame("Address"):
return fieldAddress
case s.isCaselessSame("DNS"):
return fieldDNS
case s.isCaselessSame("MTU"):
return fieldMTU
case s.isCaselessSame("Table"):
return fieldTable
case s.isCaselessSame("PublicKey"):
return fieldPublicKey
case s.isCaselessSame("PresharedKey"):
return fieldPresharedKey
case s.isCaselessSame("AllowedIPs"):
return fieldAllowedIPs
case s.isCaselessSame("Endpoint"):
return fieldEndpoint
case s.isCaselessSame("PersistentKeepalive"):
return fieldPersistentKeepalive
case s.isCaselessSame("PreUp"):
return fieldPreUp
case s.isCaselessSame("PostUp"):
return fieldPostUp
case s.isCaselessSame("PreDown"):
return fieldPreDown
case s.isCaselessSame("PostDown"):
return fieldPostDown
}
return fieldInvalid
}
func (s stringSpan) sectionType() field {
switch {
case s.isCaselessSame("[Peer]"):
return fieldPeerSection
case s.isCaselessSame("[Interface]"):
return fieldInterfaceSection
}
return fieldInvalid
}
type highlightSpanArray []highlightSpan
func (hsa *highlightSpanArray) append(o *byte, s stringSpan, t highlight) {
if s.len == 0 {
return
}
*hsa = append(*hsa, highlightSpan{t, int((uintptr(unsafe.Pointer(s.s))) - (uintptr(unsafe.Pointer(o)))), s.len})
}
func (hsa *highlightSpanArray) highlightMultivalueValue(parent, s stringSpan, section field) {
switch section {
case fieldDNS:
if s.isValidIPv4() || s.isValidIPv6() {
hsa.append(parent.s, s, highlightIP)
} else if s.isValidHostname() {
hsa.append(parent.s, s, highlightHost)
} else {
hsa.append(parent.s, s, highlightError)
}
case fieldAddress, fieldAllowedIPs:
if !s.isValidNetwork() {
hsa.append(parent.s, s, highlightError)
break
}
slash := 0
for ; slash < s.len; slash++ {
if *s.at(slash) == '/' {
break
}
}
if slash == s.len {
hsa.append(parent.s, s, highlightIP)
} else {
hsa.append(parent.s, stringSpan{s.s, slash}, highlightIP)
hsa.append(parent.s, stringSpan{s.at(slash), 1}, highlightDelimiter)
hsa.append(parent.s, stringSpan{s.at(slash + 1), s.len - slash - 1}, highlightCidr)
}
default:
hsa.append(parent.s, s, highlightError)
}
}
func (hsa *highlightSpanArray) highlightMultivalue(parent, s stringSpan, section field) {
currentSpan := stringSpan{s.s, 0}
lenAtLastSpace := 0
for i := 0; i < s.len; i++ {
if *s.at(i) == ',' {
currentSpan.len = lenAtLastSpace
hsa.highlightMultivalueValue(parent, currentSpan, section)
hsa.append(parent.s, stringSpan{s.at(i), 1}, highlightDelimiter)
lenAtLastSpace = 0
currentSpan = stringSpan{s.at(i + 1), 0}
} else if *s.at(i) == ' ' || *s.at(i) == '\t' {
if s.at(i) == currentSpan.s && currentSpan.len == 0 {
currentSpan.s = currentSpan.at(1)
} else {
currentSpan.len++
}
} else {
currentSpan.len++
lenAtLastSpace = currentSpan.len
}
}
currentSpan.len = lenAtLastSpace
if currentSpan.len != 0 {
hsa.highlightMultivalueValue(parent, currentSpan, section)
} else if (*hsa)[len(*hsa)-1].t == highlightDelimiter {
(*hsa)[len(*hsa)-1].t = highlightError
}
}
func (hsa *highlightSpanArray) highlightValue(parent, s stringSpan, section field) {
switch section {
case fieldPrivateKey:
hsa.append(parent.s, s, validateHighlight(s.isValidKey(), highlightPrivateKey))
case fieldPublicKey:
hsa.append(parent.s, s, validateHighlight(s.isValidKey(), highlightPublicKey))
case fieldJc:
hsa.append(parent.s, s, validateHighlight(s.isValidUint16(), highlightJc))
case fieldJmin:
hsa.append(parent.s, s, validateHighlight(s.isValidUint16(), highlightJmin))
case fieldJmax:
hsa.append(parent.s, s, validateHighlight(s.isValidUint16(), highlightJmax))
case fieldS1:
hsa.append(parent.s, s, validateHighlight(s.isValidUint16(), highlightS1))
case fieldS2:
hsa.append(parent.s, s, validateHighlight(s.isValidUint16(), highlightS2))
case fieldH1:
hsa.append(parent.s, s, validateHighlight(s.isValidUint32(), highlightH1))
case fieldH2:
hsa.append(parent.s, s, validateHighlight(s.isValidUint32(), highlightH2))
case fieldH3:
hsa.append(parent.s, s, validateHighlight(s.isValidUint32(), highlightH3))
case fieldH4:
hsa.append(parent.s, s, validateHighlight(s.isValidUint32(), highlightH4))
case fieldPresharedKey:
hsa.append(parent.s, s, validateHighlight(s.isValidKey(), highlightPresharedKey))
case fieldMTU:
hsa.append(parent.s, s, validateHighlight(s.isValidMTU(), highlightMTU))
case fieldTable:
hsa.append(parent.s, s, validateHighlight(s.isValidTable(), highlightTable))
case fieldPreUp, fieldPostUp, fieldPreDown, fieldPostDown:
hsa.append(parent.s, s, validateHighlight(s.isValidPrePostUpDown(), highlightCmd))
case fieldListenPort:
hsa.append(parent.s, s, validateHighlight(s.isValidPort(), highlightPort))
case fieldPersistentKeepalive:
hsa.append(parent.s, s, validateHighlight(s.isValidPersistentKeepAlive(), highlightKeepalive))
case fieldEndpoint:
if !s.isValidEndpoint() {
hsa.append(parent.s, s, highlightError)
break
}
colon := s.len
for colon > 0 {
colon--
if *s.at(colon) == ':' {
break
}
}
hsa.append(parent.s, stringSpan{s.s, colon}, highlightHost)
hsa.append(parent.s, stringSpan{s.at(colon), 1}, highlightDelimiter)
hsa.append(parent.s, stringSpan{s.at(colon + 1), s.len - colon - 1}, highlightPort)
case fieldAddress, fieldDNS, fieldAllowedIPs:
hsa.highlightMultivalue(parent, s, section)
default:
hsa.append(parent.s, s, highlightError)
}
}
func highlightConfig(config string) []highlightSpan {
var ret highlightSpanArray
b := append([]byte(config), 0)
s := stringSpan{&b[0], len(b) - 1}
currentSpan := stringSpan{s.s, 0}
currentSection := fieldInvalid
currentField := fieldInvalid
const (
onNone = iota
onKey
onValue
onComment
onSection
)
state := onNone
lenAtLastSpace := 0
equalsLocation := 0
for i := 0; i <= s.len; i++ {
if i == s.len || *s.at(i) == '\n' || state != onComment && *s.at(i) == '#' {
if state == onKey {
currentSpan.len = lenAtLastSpace
ret.append(s.s, currentSpan, highlightError)
} else if state == onValue {
if currentSpan.len != 0 {
ret.append(s.s, stringSpan{s.at(equalsLocation), 1}, highlightDelimiter)
currentSpan.len = lenAtLastSpace
ret.highlightValue(s, currentSpan, currentField)
} else {
ret.append(s.s, stringSpan{s.at(equalsLocation), 1}, highlightError)
}
} else if state == onSection {
currentSpan.len = lenAtLastSpace
currentSection = currentSpan.sectionType()
ret.append(s.s, currentSpan, validateHighlight(currentSection != fieldInvalid, highlightSection))
} else if state == onComment {
ret.append(s.s, currentSpan, highlightComment)
}
if i == s.len {
break
}
lenAtLastSpace = 0
currentField = fieldInvalid
if *s.at(i) == '#' {
currentSpan = stringSpan{s.at(i), 1}
state = onComment
} else {
currentSpan = stringSpan{s.at(i + 1), 0}
state = onNone
}
} else if state == onComment {
currentSpan.len++
} else if *s.at(i) == ' ' || *s.at(i) == '\t' {
if s.at(i) == currentSpan.s && currentSpan.len == 0 {
currentSpan.s = currentSpan.at(1)
} else {
currentSpan.len++
}
} else if *s.at(i) == '=' && state == onKey {
currentSpan.len = lenAtLastSpace
currentField = currentSpan.field()
section := sectionForField(currentField)
if section == fieldInvalid || currentField == fieldInvalid || section != currentSection {
ret.append(s.s, currentSpan, highlightError)
} else {
ret.append(s.s, currentSpan, highlightField)
}
equalsLocation = i
currentSpan = stringSpan{s.at(i + 1), 0}
state = onValue
} else {
if state == onNone {
if *s.at(i) == '[' {
state = onSection
} else {
state = onKey
}
}
currentSpan.len++
lenAtLastSpace = currentSpan.len
}
}
return ([]highlightSpan)(ret)
}

View File

@@ -1,512 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package syntax
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"syscall"
"unsafe"
"github.com/lxn/walk"
"github.com/lxn/win"
"golang.org/x/sys/windows"
)
type SyntaxEdit struct {
walk.WidgetBase
irich *win.IRichEditOle
idoc *win.ITextDocument
lastBlockState BlockState
yheight int
highlightGuard uint32
textChangedPublisher walk.EventPublisher
privateKeyPublisher walk.StringEventPublisher
blockUntunneledTrafficPublisher walk.IntEventPublisher
}
type BlockState int
const (
InevaluableBlockingUntunneledTraffic BlockState = iota
BlockingUntunneledTraffic
NotBlockingUntunneledTraffic
)
func (se *SyntaxEdit) LayoutFlags() walk.LayoutFlags {
return walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert
}
func (se *SyntaxEdit) MinSizeHint() walk.Size {
return walk.Size{20, 12}
}
func (se *SyntaxEdit) SizeHint() walk.Size {
return walk.Size{200, 100}
}
func (*SyntaxEdit) CreateLayoutItem(ctx *walk.LayoutContext) walk.LayoutItem {
return walk.NewGreedyLayoutItem()
}
func (se *SyntaxEdit) Text() string {
textLength := se.SendMessage(win.WM_GETTEXTLENGTH, 0, 0)
buf := make([]uint16, textLength+1)
se.SendMessage(win.WM_GETTEXT, uintptr(textLength+1), uintptr(unsafe.Pointer(&buf[0])))
return strings.Replace(syscall.UTF16ToString(buf), "\r\n", "\n", -1)
}
func (se *SyntaxEdit) SetText(text string) (err error) {
if text == se.Text() {
return nil
}
text = strings.Replace(text, "\n", "\r\n", -1)
if win.TRUE != se.SendMessage(win.WM_SETTEXT, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text)))) {
err = errors.New("WM_SETTEXT failed")
}
return
}
func (se *SyntaxEdit) TextChanged() *walk.Event {
return se.textChangedPublisher.Event()
}
func (se *SyntaxEdit) PrivateKeyChanged() *walk.StringEvent {
return se.privateKeyPublisher.Event()
}
func (se *SyntaxEdit) BlockUntunneledTrafficStateChanged() *walk.IntEvent {
return se.blockUntunneledTrafficPublisher.Event()
}
type spanStyle struct {
color win.COLORREF
effects uint32
}
var stylemap = map[highlight]spanStyle{
highlightSection: {color: win.RGB(0x32, 0x6D, 0x74), effects: win.CFE_BOLD},
highlightField: {color: win.RGB(0x9B, 0x23, 0x93), effects: win.CFE_BOLD},
highlightPrivateKey: {color: win.RGB(0x64, 0x38, 0x20)},
highlightPublicKey: {color: win.RGB(0x64, 0x38, 0x20)},
highlightPresharedKey: {color: win.RGB(0x64, 0x38, 0x20)},
highlightIP: {color: win.RGB(0x0E, 0x0E, 0xFF)},
highlightCidr: {color: win.RGB(0x81, 0x5F, 0x03)},
highlightHost: {color: win.RGB(0x0E, 0x0E, 0xFF)},
highlightPort: {color: win.RGB(0x81, 0x5F, 0x03)},
highlightJc: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightJmin: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightJmax: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightS1: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightS2: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightH1: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightH2: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightH3: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightH4: {color:win.RGB(0x81, 0x5F, 0x03)},
highlightMTU: {color: win.RGB(0x1C, 0x00, 0xCF)},
highlightTable: {color: win.RGB(0x1C, 0x00, 0xCF)},
highlightKeepalive: {color: win.RGB(0x1C, 0x00, 0xCF)},
highlightComment: {color: win.RGB(0x53, 0x65, 0x79), effects: win.CFE_ITALIC},
highlightDelimiter: {color: win.RGB(0x00, 0x00, 0x00)},
highlightCmd: {color: win.RGB(0x63, 0x75, 0x89)},
highlightError: {color: win.RGB(0xC4, 0x1A, 0x16), effects: win.CFE_UNDERLINE},
}
func (se *SyntaxEdit) evaluateUntunneledBlocking(cfg string, spans []highlightSpan) {
state := InevaluableBlockingUntunneledTraffic
var onAllowedIPs,
onTable,
tableOff,
seenPeer,
seen00v6,
seen00v4,
seen01v6,
seen80001v6,
seen01v4,
seen1281v4 bool
for i := range spans {
span := &spans[i]
switch span.t {
case highlightError:
goto done
case highlightSection:
if !strings.EqualFold(cfg[span.s:span.s+span.len], "[Peer]") {
break
}
if !seenPeer {
seenPeer = true
} else {
goto done
}
case highlightField:
onAllowedIPs = strings.EqualFold(cfg[span.s:span.s+span.len], "AllowedIPs")
onTable = strings.EqualFold(cfg[span.s:span.s+span.len], "Table")
case highlightTable:
if onTable {
tableOff = cfg[span.s:span.s+span.len] == "off"
}
case highlightIP:
if !onAllowedIPs || !seenPeer {
break
}
if i+2 >= len(spans) || spans[i+1].t != highlightDelimiter || spans[i+2].t != highlightCidr {
break
}
if spans[i+2].len != 1 {
break
}
switch cfg[spans[i+2].s] {
case '0':
switch cfg[span.s : span.s+span.len] {
case "0.0.0.0":
seen00v4 = true
case "::":
seen00v6 = true
}
case '1':
switch cfg[span.s : span.s+span.len] {
case "0.0.0.0":
seen01v4 = true
case "128.0.0.0":
seen1281v4 = true
case "::":
seen01v6 = true
case "8000::":
seen80001v6 = true
}
}
}
}
if tableOff {
return
}
if seen00v4 || seen00v6 {
state = BlockingUntunneledTraffic
} else if (seen01v4 && seen1281v4) || (seen01v6 && seen80001v6) {
state = NotBlockingUntunneledTraffic
}
done:
if state != se.lastBlockState {
se.blockUntunneledTrafficPublisher.Publish(int(state))
se.lastBlockState = state
}
}
func (se *SyntaxEdit) highlightText() error {
if !atomic.CompareAndSwapUint32(&se.highlightGuard, 0, 1) {
return nil
}
defer atomic.StoreUint32(&se.highlightGuard, 0)
hWnd := se.Handle()
gettextlengthex := win.GETTEXTLENGTHEX{
Flags: win.GTL_NUMBYTES,
Codepage: win.CP_ACP, // Probably CP_UTF8 would be better, but (wine at least) returns utf32 sizes.
}
msgSize := uint32(win.SendMessage(hWnd, win.EM_GETTEXTLENGTHEX, uintptr(unsafe.Pointer(&gettextlengthex)), 0))
if msgSize == win.E_INVALIDARG {
return errors.New("Failed to get text length")
}
gettextex := win.GETTEXTEX{
Flags: win.GT_NOHIDDENTEXT,
Codepage: gettextlengthex.Codepage,
Cb: msgSize + 1,
}
msg := make([]byte, msgSize+1)
msgCount := win.SendMessage(hWnd, win.EM_GETTEXTEX, uintptr(unsafe.Pointer(&gettextex)), uintptr(unsafe.Pointer(&msg[0])))
if msgCount < 0 {
return errors.New("Failed to get text")
}
cfg := strings.Replace(string(msg[:msgCount]), "\r", "\n", -1)
spans := highlightConfig(cfg)
se.evaluateUntunneledBlocking(cfg, spans)
se.idoc.Undo(win.TomSuspend, nil)
win.SendMessage(hWnd, win.EM_SETEVENTMASK, 0, 0)
win.SendMessage(hWnd, win.WM_SETREDRAW, win.FALSE, 0)
var origSelection win.CHARRANGE
win.SendMessage(hWnd, win.EM_EXGETSEL, 0, uintptr(unsafe.Pointer(&origSelection)))
var origScroll win.POINT
win.SendMessage(hWnd, win.EM_GETSCROLLPOS, 0, uintptr(unsafe.Pointer(&origScroll)))
win.SendMessage(hWnd, win.EM_HIDESELECTION, win.TRUE, 0)
format := win.CHARFORMAT2{
CHARFORMAT: win.CHARFORMAT{
CbSize: uint32(unsafe.Sizeof(win.CHARFORMAT2{})),
DwMask: win.CFM_COLOR | win.CFM_CHARSET | win.CFM_SIZE | win.CFM_BOLD | win.CFM_ITALIC | win.CFM_UNDERLINE,
DwEffects: win.CFE_AUTOCOLOR,
BCharSet: win.ANSI_CHARSET,
},
}
if se.yheight != 0 {
format.YHeight = 20 * 10
}
win.SendMessage(hWnd, win.EM_SETCHARFORMAT, win.SCF_ALL, uintptr(unsafe.Pointer(&format)))
bgColor := win.COLORREF(win.GetSysColor(win.COLOR_WINDOW))
bgInversion := (bgColor & win.RGB(0xFF, 0xFF, 0xFF)) ^ win.RGB(0xFF, 0xFF, 0xFF)
win.SendMessage(hWnd, win.EM_SETBKGNDCOLOR, 0, uintptr(bgColor))
numSpans := len(spans)
foundPrivateKey := false
for i := range spans {
span := &spans[i]
if numSpans <= 2048 {
selection := win.CHARRANGE{int32(span.s), int32(span.s + span.len)}
win.SendMessage(hWnd, win.EM_EXSETSEL, 0, uintptr(unsafe.Pointer(&selection)))
format.CrTextColor = stylemap[span.t].color ^ bgInversion
format.DwEffects = stylemap[span.t].effects
win.SendMessage(hWnd, win.EM_SETCHARFORMAT, win.SCF_SELECTION, uintptr(unsafe.Pointer(&format)))
}
if span.t == highlightPrivateKey && !foundPrivateKey {
privateKey := cfg[span.s : span.s+span.len]
se.privateKeyPublisher.Publish(privateKey)
foundPrivateKey = true
}
}
win.SendMessage(hWnd, win.EM_SETSCROLLPOS, 0, uintptr(unsafe.Pointer(&origScroll)))
win.SendMessage(hWnd, win.EM_EXSETSEL, 0, uintptr(unsafe.Pointer(&origSelection)))
win.SendMessage(hWnd, win.EM_HIDESELECTION, win.FALSE, 0)
win.SendMessage(hWnd, win.WM_SETREDRAW, win.TRUE, 0)
win.RedrawWindow(hWnd, nil, 0, win.RDW_ERASE|win.RDW_FRAME|win.RDW_INVALIDATE|win.RDW_ALLCHILDREN)
win.SendMessage(hWnd, win.EM_SETEVENTMASK, 0, win.ENM_CHANGE)
se.idoc.Undo(win.TomResume, nil)
if !foundPrivateKey {
se.privateKeyPublisher.Publish("")
}
return nil
}
func (se *SyntaxEdit) contextMenu(x, y int32) error {
/* This disturbing hack grabs the system edit menu normally used for the EDIT control. */
comctl32UTF16, err := windows.UTF16PtrFromString("comctl32.dll")
if err != nil {
return err
}
comctl32Handle := win.GetModuleHandle(comctl32UTF16)
if comctl32Handle == 0 {
return errors.New("Failed to get comctl32.dll handle")
}
menu := win.LoadMenu(comctl32Handle, win.MAKEINTRESOURCE(1))
if menu == 0 {
return errors.New("Failed to load menu")
}
defer win.DestroyMenu(menu)
hWnd := se.Handle()
enableWhenSelected := uint32(win.MF_GRAYED)
var selection win.CHARRANGE
win.SendMessage(hWnd, win.EM_EXGETSEL, 0, uintptr(unsafe.Pointer(&selection)))
if selection.CpMin < selection.CpMax {
enableWhenSelected = win.MF_ENABLED
}
enableSelectAll := uint32(win.MF_GRAYED)
gettextlengthex := win.GETTEXTLENGTHEX{
Flags: win.GTL_DEFAULT,
Codepage: win.CP_ACP,
}
if selection.CpMin != 0 || (selection.CpMax < int32(win.SendMessage(hWnd, win.EM_GETTEXTLENGTHEX, uintptr(unsafe.Pointer(&gettextlengthex)), 0))) {
enableSelectAll = win.MF_ENABLED
}
enableUndo := uint32(win.MF_GRAYED)
if win.SendMessage(hWnd, win.EM_CANUNDO, 0, 0) != 0 {
enableUndo = win.MF_ENABLED
}
enablePaste := uint32(win.MF_GRAYED)
if win.SendMessage(hWnd, win.EM_CANPASTE, win.CF_TEXT, 0) != 0 {
enablePaste = win.MF_ENABLED
}
popup := win.GetSubMenu(menu, 0)
win.EnableMenuItem(popup, win.WM_UNDO, win.MF_BYCOMMAND|enableUndo)
win.EnableMenuItem(popup, win.WM_CUT, win.MF_BYCOMMAND|enableWhenSelected)
win.EnableMenuItem(popup, win.WM_COPY, win.MF_BYCOMMAND|enableWhenSelected)
win.EnableMenuItem(popup, win.WM_PASTE, win.MF_BYCOMMAND|enablePaste)
win.EnableMenuItem(popup, win.WM_CLEAR, win.MF_BYCOMMAND|enableWhenSelected)
win.EnableMenuItem(popup, win.EM_SETSEL, win.MF_BYCOMMAND|enableSelectAll)
// Delete items that we don't handle.
for ctl := win.GetMenuItemCount(popup) - 1; ctl >= 0; ctl-- {
menuItem := win.MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})),
FMask: win.MIIM_FTYPE | win.MIIM_ID,
}
if !win.GetMenuItemInfo(popup, uint32(ctl), win.MF_BYPOSITION, &menuItem) {
continue
}
if (menuItem.FType & win.MFT_SEPARATOR) != 0 {
continue
}
switch menuItem.WID {
case win.WM_UNDO, win.WM_CUT, win.WM_COPY, win.WM_PASTE, win.WM_CLEAR, win.EM_SETSEL:
continue
}
win.DeleteMenu(popup, uint32(ctl), win.MF_BYPOSITION)
}
// Delete trailing and adjacent separators.
end := true
for ctl := win.GetMenuItemCount(popup) - 1; ctl >= 0; ctl-- {
menuItem := win.MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})),
FMask: win.MIIM_FTYPE,
}
if !win.GetMenuItemInfo(popup, uint32(ctl), win.MF_BYPOSITION, &menuItem) {
continue
}
if (menuItem.FType & win.MFT_SEPARATOR) == 0 {
end = false
continue
}
if !end && ctl > 0 {
if !win.GetMenuItemInfo(popup, uint32(ctl-1), win.MF_BYPOSITION, &menuItem) {
continue
}
if (menuItem.FType & win.MFT_SEPARATOR) == 0 {
continue
}
}
win.DeleteMenu(popup, uint32(ctl), win.MF_BYPOSITION)
}
if x == -1 && y == -1 {
var rect win.RECT
win.GetWindowRect(hWnd, &rect)
x = (rect.Left + rect.Right) / 2
y = (rect.Top + rect.Bottom) / 2
}
if win.GetFocus() != hWnd {
win.SetFocus(hWnd)
}
cmd := win.TrackPopupMenu(popup, win.TPM_LEFTALIGN|win.TPM_RIGHTBUTTON|win.TPM_RETURNCMD|win.TPM_NONOTIFY, x, y, 0, hWnd, nil)
if cmd != 0 {
lParam := uintptr(0)
if cmd == win.EM_SETSEL {
lParam = ^uintptr(0)
}
win.SendMessage(hWnd, cmd, 0, lParam)
}
return nil
}
func (*SyntaxEdit) NeedsWmSize() bool {
return true
}
func (se *SyntaxEdit) WndProc(hWnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case win.WM_DESTROY:
if se.idoc != nil {
se.idoc.Release()
}
if se.irich != nil {
se.irich.Release()
}
case win.WM_SETTEXT:
ret := se.WidgetBase.WndProc(hWnd, msg, wParam, lParam)
se.highlightText()
win.SendMessage(hWnd, win.EM_EMPTYUNDOBUFFER, 0, 0)
se.textChangedPublisher.Publish()
return ret
case win.WM_COMMAND, win.WM_NOTIFY:
switch win.HIWORD(uint32(wParam)) {
case win.EN_CHANGE:
se.highlightText()
se.textChangedPublisher.Publish()
}
case win.WM_PASTE:
win.SendMessage(hWnd, win.EM_PASTESPECIAL, win.CF_TEXT, 0)
return 0
case win.WM_KEYDOWN:
key := win.LOWORD(uint32(wParam))
if key == 'V' && win.GetKeyState(win.VK_CONTROL) < 0 ||
key == win.VK_INSERT && win.GetKeyState(win.VK_SHIFT) < 0 {
win.SendMessage(hWnd, win.EM_PASTESPECIAL, win.CF_TEXT, 0)
return 0
}
case win.WM_CONTEXTMENU:
se.contextMenu(win.GET_X_LPARAM(lParam), win.GET_Y_LPARAM(lParam))
return 0
case win.WM_THEMECHANGED:
se.highlightText()
case win.WM_GETDLGCODE:
m := (*win.MSG)(unsafe.Pointer(lParam))
ret := se.WidgetBase.WndProc(hWnd, msg, wParam, lParam)
ret &^= win.DLGC_WANTTAB
if m != nil && m.Message == win.WM_KEYDOWN && m.WParam == win.VK_TAB && win.GetKeyState(win.VK_CONTROL) >= 0 {
ret &^= win.DLGC_WANTMESSAGE
}
return ret
}
return se.WidgetBase.WndProc(hWnd, msg, wParam, lParam)
}
func NewSyntaxEdit(parent walk.Container) (*SyntaxEdit, error) {
const LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
_, err := windows.LoadLibraryEx("msftedit.dll", 0, LOAD_LIBRARY_SEARCH_SYSTEM32)
if err != nil {
return nil, fmt.Errorf("Failed to load msftedit.dll: %w", err)
}
se := &SyntaxEdit{}
if err := walk.InitWidget(
se,
parent,
win.MSFTEDIT_CLASS,
win.WS_CHILD|win.ES_MULTILINE|win.WS_VISIBLE|win.WS_VSCROLL|win.WS_BORDER|win.WS_HSCROLL|win.WS_TABSTOP|win.ES_WANTRETURN|win.ES_NOOLEDRAGDROP,
0); err != nil {
return nil, err
}
hWnd := se.Handle()
win.SetWindowLong(hWnd, win.GWL_EXSTYLE, win.GetWindowLong(hWnd, win.GWL_EXSTYLE)&^win.WS_EX_CLIENTEDGE)
win.SendMessage(hWnd, win.EM_GETOLEINTERFACE, 0, uintptr(unsafe.Pointer(&se.irich)))
var idoc unsafe.Pointer
se.irich.QueryInterface(&win.IID_ITextDocument, &idoc)
se.idoc = (*win.ITextDocument)(idoc)
win.SendMessage(hWnd, win.EM_SETEVENTMASK, 0, win.ENM_CHANGE)
win.SendMessage(hWnd, win.EM_SETTEXTMODE, win.TM_SINGLECODEPAGE, 0)
se.ApplyDPI(parent.DPI())
se.GraphicsEffects().Add(walk.InteractionEffect)
se.GraphicsEffects().Add(walk.FocusEffect)
se.MustRegisterProperty("Text", walk.NewProperty(
func() any {
return se.Text()
},
func(v any) error {
if s, ok := v.(string); ok {
return se.SetText(s)
}
return se.SetText("")
},
se.textChangedPublisher.Event()))
return se, nil
}
func (se *SyntaxEdit) ApplyDPI(dpi int) {
hWnd := se.Handle()
hdc := win.GetDC(hWnd)
logPixels := win.GetDeviceCaps(hdc, win.LOGPIXELSY)
if se.yheight != 0 {
win.SendMessage(hWnd, win.EM_SETZOOM, uintptr(logPixels), uintptr(dpi))
}
se.yheight = 20 * 10 * dpi / int(logPixels)
win.ReleaseDC(hWnd, hdc)
se.highlightText()
}

View File

@@ -1,392 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"sort"
"strings"
"time"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/l18n"
"golang.zx2c4.com/wireguard/windows/manager"
"github.com/lxn/walk"
)
// Status + active CIDRs + separator
const trayTunnelActionsOffset = 3
type Tray struct {
*walk.NotifyIcon
// Current known tunnels by name
tunnels map[string]*walk.Action
tunnelsAreInBreakoutMenu bool
mtw *ManageTunnelsWindow
tunnelChangedCB *manager.TunnelChangeCallback
tunnelsChangedCB *manager.TunnelsChangeCallback
clicked func()
}
func NewTray(mtw *ManageTunnelsWindow) (*Tray, error) {
var err error
tray := &Tray{
mtw: mtw,
tunnels: make(map[string]*walk.Action),
}
tray.NotifyIcon, err = walk.NewNotifyIcon(mtw)
if err != nil {
return nil, err
}
return tray, tray.setup()
}
func (tray *Tray) setup() error {
tray.clicked = tray.onManageTunnels
tray.SetToolTip(l18n.Sprintf("WireGuard: Deactivated"))
tray.SetVisible(true)
if icon, err := loadLogoIcon(16); err == nil {
tray.SetIcon(icon)
}
tray.MouseDown().Attach(func(x, y int, button walk.MouseButton) {
if button == walk.LeftButton {
tray.clicked()
}
})
tray.MessageClicked().Attach(func() {
tray.clicked()
})
for _, item := range [...]struct {
label string
handler walk.EventHandler
enabled bool
hidden bool
separator bool
defawlt bool
}{
{label: l18n.Sprintf("Status: Unknown")},
{label: l18n.Sprintf("Addresses: None"), hidden: true},
{separator: true},
{separator: true},
{label: l18n.Sprintf("&Manage tunnels…"), handler: tray.onManageTunnels, enabled: true, defawlt: true},
{label: l18n.Sprintf("&Import tunnel(s) from file…"), handler: tray.onImport, enabled: true, hidden: !IsAdmin},
{separator: true},
{label: l18n.Sprintf("&About WireGuard…"), handler: tray.onAbout, enabled: true},
{label: l18n.Sprintf("E&xit"), handler: onQuit, enabled: true, hidden: !IsAdmin},
} {
var action *walk.Action
if item.separator {
action = walk.NewSeparatorAction()
} else {
action = walk.NewAction()
action.SetText(item.label)
action.SetEnabled(item.enabled)
action.SetVisible(!item.hidden)
action.SetDefault(item.defawlt)
if item.handler != nil {
action.Triggered().Attach(item.handler)
}
}
tray.ContextMenu().Actions().Add(action)
}
tray.tunnelChangedCB = manager.IPCClientRegisterTunnelChange(tray.onTunnelChange)
tray.tunnelsChangedCB = manager.IPCClientRegisterTunnelsChange(tray.onTunnelsChange)
tray.onTunnelsChange()
globalState, _ := manager.IPCClientGlobalState()
tray.updateGlobalState(globalState)
return nil
}
func (tray *Tray) Dispose() error {
if tray.tunnelChangedCB != nil {
tray.tunnelChangedCB.Unregister()
tray.tunnelChangedCB = nil
}
if tray.tunnelsChangedCB != nil {
tray.tunnelsChangedCB.Unregister()
tray.tunnelsChangedCB = nil
}
return tray.NotifyIcon.Dispose()
}
func (tray *Tray) onTunnelsChange() {
tunnels, err := manager.IPCClientTunnels()
if err != nil {
return
}
tray.mtw.Synchronize(func() {
tunnelSet := make(map[string]bool, len(tunnels))
for _, tunnel := range tunnels {
tunnelSet[tunnel.Name] = true
if tray.tunnels[tunnel.Name] == nil {
tray.addTunnelAction(&tunnel)
}
}
for trayTunnel := range tray.tunnels {
if !tunnelSet[trayTunnel] {
tray.removeTunnelAction(trayTunnel)
}
}
})
}
func (tray *Tray) sortedTunnels() []string {
var names []string
for name := range tray.tunnels {
names = append(names, name)
}
sort.SliceStable(names, func(i, j int) bool {
return conf.TunnelNameIsLess(names[i], names[j])
})
return names
}
func (tray *Tray) addTunnelAction(tunnel *manager.Tunnel) {
tunnelAction := walk.NewAction()
tunnelAction.SetText(tunnel.Name)
tunnelAction.SetEnabled(true)
tunnelAction.SetCheckable(true)
tclosure := *tunnel
tunnelAction.Triggered().Attach(func() {
tunnelAction.SetChecked(!tunnelAction.Checked())
go func() {
oldState, err := tclosure.Toggle()
if err != nil {
tray.mtw.Synchronize(func() {
raise(tray.mtw.Handle())
tray.mtw.tunnelsPage.listView.selectTunnel(tclosure.Name)
tray.mtw.tabs.SetCurrentIndex(0)
if oldState == manager.TunnelUnknown {
showErrorCustom(tray.mtw, l18n.Sprintf("Failed to determine tunnel state"), err.Error())
} else if oldState == manager.TunnelStopped {
showErrorCustom(tray.mtw, l18n.Sprintf("Failed to activate tunnel"), err.Error())
} else if oldState == manager.TunnelStarted {
showErrorCustom(tray.mtw, l18n.Sprintf("Failed to deactivate tunnel"), err.Error())
}
})
}
}()
})
tray.tunnels[tunnel.Name] = tunnelAction
var (
idx int
name string
)
for idx, name = range tray.sortedTunnels() {
if name == tunnel.Name {
break
}
}
if tray.tunnelsAreInBreakoutMenu {
tray.ContextMenu().Actions().At(trayTunnelActionsOffset).Menu().Actions().Insert(idx, tunnelAction)
} else {
tray.ContextMenu().Actions().Insert(trayTunnelActionsOffset+idx, tunnelAction)
}
tray.rebalanceTunnelsMenu()
go func() {
state, err := tclosure.State()
if err != nil {
return
}
tray.mtw.Synchronize(func() {
tray.setTunnelState(&tclosure, state)
})
}()
}
func (tray *Tray) removeTunnelAction(tunnelName string) {
if tray.tunnelsAreInBreakoutMenu {
tray.ContextMenu().Actions().At(trayTunnelActionsOffset).Menu().Actions().Remove(tray.tunnels[tunnelName])
} else {
tray.ContextMenu().Actions().Remove(tray.tunnels[tunnelName])
}
delete(tray.tunnels, tunnelName)
tray.rebalanceTunnelsMenu()
}
func (tray *Tray) rebalanceTunnelsMenu() {
if tray.tunnelsAreInBreakoutMenu && len(tray.tunnels) <= 10 {
menuAction := tray.ContextMenu().Actions().At(trayTunnelActionsOffset)
idx := 1
for _, name := range tray.sortedTunnels() {
tray.ContextMenu().Actions().Insert(trayTunnelActionsOffset+idx, tray.tunnels[name])
idx++
}
tray.ContextMenu().Actions().Remove(menuAction)
menuAction.Menu().Dispose()
tray.tunnelsAreInBreakoutMenu = false
} else if !tray.tunnelsAreInBreakoutMenu && len(tray.tunnels) > 10 {
menu, err := walk.NewMenu()
if err != nil {
return
}
for _, name := range tray.sortedTunnels() {
action := tray.tunnels[name]
menu.Actions().Add(action)
tray.ContextMenu().Actions().Remove(action)
}
menuAction, err := tray.ContextMenu().Actions().InsertMenu(trayTunnelActionsOffset, menu)
if err != nil {
return
}
menuAction.SetText(l18n.Sprintf("&Tunnels"))
tray.tunnelsAreInBreakoutMenu = true
}
}
func (tray *Tray) onTunnelChange(tunnel *manager.Tunnel, state, globalState manager.TunnelState, err error) {
tray.mtw.Synchronize(func() {
tray.updateGlobalState(globalState)
if err == nil {
tunnelAction := tray.tunnels[tunnel.Name]
if tunnelAction != nil {
wasChecked := tunnelAction.Checked()
switch state {
case manager.TunnelStarted:
if !wasChecked {
icon, _ := iconWithOverlayForState(state, 128)
tray.ShowCustom(l18n.Sprintf("WireGuard Activated"), l18n.Sprintf("The %s tunnel has been activated.", tunnel.Name), icon)
}
case manager.TunnelStopped:
if wasChecked {
icon, _ := loadSystemIcon("imageres", -31, 128) // TODO: this icon isn't very good...
tray.ShowCustom(l18n.Sprintf("WireGuard Deactivated"), l18n.Sprintf("The %s tunnel has been deactivated.", tunnel.Name), icon)
}
}
}
} else if !tray.mtw.Visible() {
tray.ShowError(l18n.Sprintf("WireGuard Tunnel Error"), err.Error())
}
tray.setTunnelState(tunnel, state)
})
}
func (tray *Tray) updateGlobalState(globalState manager.TunnelState) {
if icon, err := iconWithOverlayForState(globalState, 16); err == nil {
tray.SetIcon(icon)
}
actions := tray.ContextMenu().Actions()
statusAction := actions.At(0)
tray.SetToolTip(l18n.Sprintf("WireGuard: %s", textForState(globalState, true)))
stateText := textForState(globalState, false)
stateIcon, err := iconForState(globalState, 16)
if err == nil {
statusAction.SetImage(stateIcon)
}
statusAction.SetText(l18n.Sprintf("Status: %s", stateText))
go func() {
var addrs []string
tunnels, err := manager.IPCClientTunnels()
if err == nil {
for i := range tunnels {
state, err := tunnels[i].State()
if err == nil && state == manager.TunnelStarted {
config, err := tunnels[i].RuntimeConfig()
if err == nil {
for _, addr := range config.Interface.Addresses {
addrs = append(addrs, addr.String())
}
}
}
}
}
tray.mtw.Synchronize(func() {
activeCIDRsAction := tray.ContextMenu().Actions().At(1)
activeCIDRsAction.SetText(l18n.Sprintf("Addresses: %s", strings.Join(addrs, l18n.EnumerationSeparator())))
activeCIDRsAction.SetVisible(len(addrs) > 0)
})
}()
for _, action := range tray.tunnels {
action.SetEnabled(globalState == manager.TunnelStarted || globalState == manager.TunnelStopped)
}
}
func (tray *Tray) setTunnelState(tunnel *manager.Tunnel, state manager.TunnelState) {
tunnelAction := tray.tunnels[tunnel.Name]
if tunnelAction == nil {
return
}
switch state {
case manager.TunnelStarted:
tunnelAction.SetEnabled(true)
tunnelAction.SetChecked(true)
case manager.TunnelStopped:
tunnelAction.SetChecked(false)
}
}
func (tray *Tray) UpdateFound() {
action := walk.NewAction()
action.SetText(l18n.Sprintf("An Update is Available!"))
menuIcon, _ := loadShieldIcon(16)
action.SetImage(menuIcon)
action.SetDefault(true)
showUpdateTab := func() {
if !tray.mtw.Visible() {
tray.mtw.tunnelsPage.listView.SelectFirstActiveTunnel()
}
tray.mtw.tabs.SetCurrentIndex(2)
raise(tray.mtw.Handle())
}
action.Triggered().Attach(showUpdateTab)
tray.clicked = showUpdateTab
tray.ContextMenu().Actions().Insert(tray.ContextMenu().Actions().Len()-2, action)
showUpdateBalloon := func() {
icon, _ := loadShieldIcon(128)
tray.ShowCustom(l18n.Sprintf("WireGuard Update Available"), l18n.Sprintf("An update to WireGuard is now available. You are advised to update as soon as possible."), icon)
}
timeSinceStart := time.Now().Sub(startTime)
if timeSinceStart < time.Second*3 {
time.AfterFunc(time.Second*3-timeSinceStart, func() {
tray.mtw.Synchronize(showUpdateBalloon)
})
} else {
showUpdateBalloon()
}
}
func (tray *Tray) onManageTunnels() {
tray.mtw.tunnelsPage.listView.SelectFirstActiveTunnel()
tray.mtw.tabs.SetCurrentIndex(0)
raise(tray.mtw.Handle())
}
func (tray *Tray) onAbout() {
if tray.mtw.Visible() {
onAbout(tray.mtw)
} else {
onAbout(nil)
}
}
func (tray *Tray) onImport() {
raise(tray.mtw.Handle())
tray.mtw.tunnelsPage.onImport()
}

View File

@@ -1,606 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"archive/zip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/lxn/walk"
"github.com/amnezia-vpn/awg-windows/conf"
"github.com/amnezia-vpn/awg-windows/l18n"
"golang.zx2c4.com/wireguard/windows/manager"
)
type TunnelsPage struct {
*walk.TabPage
listView *ListView
listContainer walk.Container
listToolbar *walk.ToolBar
confView *ConfView
fillerButton *walk.PushButton
fillerHandler func()
fillerContainer *walk.Composite
currentTunnelContainer *walk.Composite
}
func NewTunnelsPage() (*TunnelsPage, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
tp := new(TunnelsPage)
if tp.TabPage, err = walk.NewTabPage(); err != nil {
return nil, err
}
disposables.Add(tp)
tp.SetTitle(l18n.Sprintf("Tunnels"))
tp.SetLayout(walk.NewHBoxLayout())
tp.listContainer, _ = walk.NewComposite(tp)
vlayout := walk.NewVBoxLayout()
vlayout.SetMargins(walk.Margins{})
vlayout.SetSpacing(0)
tp.listContainer.SetLayout(vlayout)
if tp.listView, err = NewListView(tp.listContainer); err != nil {
return nil, err
}
if tp.currentTunnelContainer, err = walk.NewComposite(tp); err != nil {
return nil, err
}
vlayout = walk.NewVBoxLayout()
vlayout.SetMargins(walk.Margins{})
tp.currentTunnelContainer.SetLayout(vlayout)
if tp.fillerContainer, err = walk.NewComposite(tp); err != nil {
return nil, err
}
tp.fillerContainer.SetVisible(false)
hlayout := walk.NewHBoxLayout()
hlayout.SetMargins(walk.Margins{})
tp.fillerContainer.SetLayout(hlayout)
tp.fillerButton, _ = walk.NewPushButton(tp.fillerContainer)
tp.fillerButton.SetMinMaxSize(walk.Size{200, 0}, walk.Size{200, 0})
tp.fillerButton.SetVisible(IsAdmin)
tp.fillerButton.Clicked().Attach(func() {
if tp.fillerHandler != nil {
tp.fillerHandler()
}
})
if tp.confView, err = NewConfView(tp.currentTunnelContainer); err != nil {
return nil, err
}
controlsContainer, err := walk.NewComposite(tp.currentTunnelContainer)
if err != nil {
return nil, err
}
hlayout = walk.NewHBoxLayout()
hlayout.SetMargins(walk.Margins{})
controlsContainer.SetLayout(hlayout)
walk.NewHSpacer(controlsContainer)
editTunnel, err := walk.NewPushButton(controlsContainer)
if err != nil {
return nil, err
}
editTunnel.SetEnabled(false)
tp.listView.CurrentIndexChanged().Attach(func() {
editTunnel.SetEnabled(tp.listView.CurrentIndex() > -1)
})
editTunnel.SetText(l18n.Sprintf("&Edit"))
editTunnel.Clicked().Attach(tp.onEditTunnel)
editTunnel.SetVisible(IsAdmin)
disposables.Spare()
tp.listView.ItemCountChanged().Attach(tp.onTunnelsChanged)
tp.listView.SelectedIndexesChanged().Attach(tp.onSelectedTunnelsChanged)
tp.listView.ItemActivated().Attach(tp.onTunnelsViewItemActivated)
tp.listView.CurrentIndexChanged().Attach(tp.updateConfView)
tp.listView.Load(false)
tp.onTunnelsChanged()
return tp, nil
}
func (tp *TunnelsPage) CreateToolbar() error {
if tp.listToolbar != nil {
return nil
}
// HACK: Because of https://github.com/lxn/walk/issues/481
// we need to put the ToolBar into its own Composite.
toolBarContainer, err := walk.NewComposite(tp.listContainer)
if err != nil {
return err
}
toolBarContainer.SetDoubleBuffering(true)
hlayout := walk.NewHBoxLayout()
hlayout.SetMargins(walk.Margins{})
toolBarContainer.SetLayout(hlayout)
toolBarContainer.SetVisible(IsAdmin)
if tp.listToolbar, err = walk.NewToolBarWithOrientationAndButtonStyle(toolBarContainer, walk.Horizontal, walk.ToolBarButtonImageBeforeText); err != nil {
return err
}
addMenu, err := walk.NewMenu()
if err != nil {
return err
}
tp.AddDisposable(addMenu)
importAction := walk.NewAction()
importAction.SetText(l18n.Sprintf("&Import tunnel(s) from file…"))
importActionIcon, _ := loadSystemIcon("imageres", -3, 16)
importAction.SetImage(importActionIcon)
importAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyO})
importAction.SetDefault(true)
importAction.Triggered().Attach(tp.onImport)
addMenu.Actions().Add(importAction)
addAction := walk.NewAction()
addAction.SetText(l18n.Sprintf("Add &empty tunnel…"))
addActionIcon, _ := loadSystemIcon("imageres", -2, 16)
addAction.SetImage(addActionIcon)
addAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyN})
addAction.Triggered().Attach(tp.onAddTunnel)
addMenu.Actions().Add(addAction)
addMenuAction := walk.NewMenuAction(addMenu)
addMenuActionIcon, _ := loadSystemIcon("shell32", -258, 16)
addMenuAction.SetImage(addMenuActionIcon)
addMenuAction.SetText(l18n.Sprintf("Add Tunnel"))
addMenuAction.SetToolTip(importAction.Text())
addMenuAction.Triggered().Attach(tp.onImport)
tp.listToolbar.Actions().Add(addMenuAction)
tp.listToolbar.Actions().Add(walk.NewSeparatorAction())
deleteAction := walk.NewAction()
deleteActionIcon, _ := loadSystemIcon("shell32", -240, 16)
deleteAction.SetImage(deleteActionIcon)
deleteAction.SetShortcut(walk.Shortcut{0, walk.KeyDelete})
deleteAction.SetToolTip(l18n.Sprintf("Remove selected tunnel(s)"))
deleteAction.Triggered().Attach(tp.onDelete)
tp.listToolbar.Actions().Add(deleteAction)
tp.listToolbar.Actions().Add(walk.NewSeparatorAction())
exportAction := walk.NewAction()
exportActionIcon, _ := loadSystemIcon("imageres", -174, 16)
exportAction.SetImage(exportActionIcon)
exportAction.SetToolTip(l18n.Sprintf("Export all tunnels to zip"))
exportAction.Triggered().Attach(tp.onExportTunnels)
tp.listToolbar.Actions().Add(exportAction)
fixContainerWidthToToolbarWidth := func() {
toolbarWidth := tp.listToolbar.SizeHint().Width
tp.listContainer.SetMinMaxSizePixels(walk.Size{toolbarWidth, 0}, walk.Size{toolbarWidth, 0})
}
fixContainerWidthToToolbarWidth()
tp.listToolbar.SizeChanged().Attach(fixContainerWidthToToolbarWidth)
contextMenu, err := walk.NewMenu()
if err != nil {
return err
}
tp.listView.AddDisposable(contextMenu)
toggleAction := walk.NewAction()
toggleAction.SetText(l18n.Sprintf("&Toggle"))
toggleAction.SetDefault(true)
toggleAction.Triggered().Attach(tp.onTunnelsViewItemActivated)
contextMenu.Actions().Add(toggleAction)
contextMenu.Actions().Add(walk.NewSeparatorAction())
importAction2 := walk.NewAction()
importAction2.SetText(l18n.Sprintf("&Import tunnel(s) from file…"))
importAction2.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyO})
importAction2.Triggered().Attach(tp.onImport)
importAction2.SetVisible(IsAdmin)
contextMenu.Actions().Add(importAction2)
tp.ShortcutActions().Add(importAction2)
addAction2 := walk.NewAction()
addAction2.SetText(l18n.Sprintf("Add &empty tunnel…"))
addAction2.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyN})
addAction2.Triggered().Attach(tp.onAddTunnel)
addAction2.SetVisible(IsAdmin)
contextMenu.Actions().Add(addAction2)
tp.ShortcutActions().Add(addAction2)
exportAction2 := walk.NewAction()
exportAction2.SetText(l18n.Sprintf("Export all tunnels to &zip…"))
exportAction2.Triggered().Attach(tp.onExportTunnels)
exportAction2.SetVisible(IsAdmin)
contextMenu.Actions().Add(exportAction2)
contextMenu.Actions().Add(walk.NewSeparatorAction())
editAction := walk.NewAction()
editAction.SetText(l18n.Sprintf("Edit &selected tunnel…"))
editAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyE})
editAction.SetVisible(IsAdmin)
editAction.Triggered().Attach(tp.onEditTunnel)
contextMenu.Actions().Add(editAction)
tp.ShortcutActions().Add(editAction)
deleteAction2 := walk.NewAction()
deleteAction2.SetText(l18n.Sprintf("&Remove selected tunnel(s)"))
deleteAction2.SetShortcut(walk.Shortcut{0, walk.KeyDelete})
deleteAction2.SetVisible(IsAdmin)
deleteAction2.Triggered().Attach(tp.onDelete)
contextMenu.Actions().Add(deleteAction2)
tp.listView.ShortcutActions().Add(deleteAction2)
selectAllAction := walk.NewAction()
selectAllAction.SetText(l18n.Sprintf("Select &all"))
selectAllAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyA})
selectAllAction.SetVisible(IsAdmin)
selectAllAction.Triggered().Attach(tp.onSelectAll)
contextMenu.Actions().Add(selectAllAction)
tp.listView.ShortcutActions().Add(selectAllAction)
tp.listView.SetContextMenu(contextMenu)
setSelectionOrientedOptions := func() {
selected := len(tp.listView.SelectedIndexes())
all := len(tp.listView.model.tunnels)
deleteAction.SetEnabled(selected > 0)
deleteAction2.SetEnabled(selected > 0)
toggleAction.SetEnabled(selected == 1)
selectAllAction.SetEnabled(selected < all)
editAction.SetEnabled(selected == 1)
}
tp.listView.SelectedIndexesChanged().Attach(setSelectionOrientedOptions)
setSelectionOrientedOptions()
setExport := func() {
all := len(tp.listView.model.tunnels)
exportAction.SetEnabled(all > 0)
exportAction2.SetEnabled(all > 0)
}
setExportRange := func(from, to int) { setExport() }
tp.listView.model.RowsInserted().Attach(setExportRange)
tp.listView.model.RowsRemoved().Attach(setExportRange)
tp.listView.model.RowsReset().Attach(setExport)
setExport()
return nil
}
func (tp *TunnelsPage) updateConfView() {
tp.confView.SetTunnel(tp.listView.CurrentTunnel())
}
func (tp *TunnelsPage) importFiles(paths []string) {
go func() {
syncedMsgBox := func(title, message string, flags walk.MsgBoxStyle) {
tp.Synchronize(func() {
walk.MsgBox(tp.Form(), title, message, flags)
})
}
type unparsedConfig struct {
Name string
Config string
}
var (
unparsedConfigs []unparsedConfig
lastErr error
)
for _, path := range paths {
switch strings.ToLower(filepath.Ext(path)) {
case ".conf":
textConfig, err := os.ReadFile(path)
if err != nil {
lastErr = err
continue
}
unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)), Config: string(textConfig)})
case ".zip":
// 1 .conf + 1 error .zip edge case?
r, err := zip.OpenReader(path)
if err != nil {
lastErr = err
continue
}
for _, f := range r.File {
if strings.ToLower(filepath.Ext(f.Name)) != ".conf" {
continue
}
rc, err := f.Open()
if err != nil {
lastErr = err
continue
}
textConfig, err := io.ReadAll(rc)
rc.Close()
if err != nil {
lastErr = err
continue
}
unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)), Config: string(textConfig)})
}
r.Close()
}
}
if lastErr != nil || unparsedConfigs == nil {
if lastErr == nil {
lastErr = errors.New(l18n.Sprintf("no configuration files were found"))
}
syncedMsgBox(l18n.Sprintf("Error"), l18n.Sprintf("Could not import selected configuration: %v", lastErr), walk.MsgBoxIconWarning)
return
}
// Add in reverse order so that the first one is selected.
sort.Slice(unparsedConfigs, func(i, j int) bool {
return conf.TunnelNameIsLess(unparsedConfigs[j].Name, unparsedConfigs[i].Name)
})
existingTunnelList, err := manager.IPCClientTunnels()
if err != nil {
syncedMsgBox(l18n.Sprintf("Error"), l18n.Sprintf("Could not enumerate existing tunnels: %v", lastErr), walk.MsgBoxIconWarning)
return
}
existingLowerTunnels := make(map[string]bool, len(existingTunnelList))
for _, tunnel := range existingTunnelList {
existingLowerTunnels[strings.ToLower(tunnel.Name)] = true
}
configCount := 0
tp.listView.SetSuspendTunnelsUpdate(true)
for _, unparsedConfig := range unparsedConfigs {
if existingLowerTunnels[strings.ToLower(unparsedConfig.Name)] {
lastErr = errors.New(l18n.Sprintf("Another tunnel already exists with the name %s", unparsedConfig.Name))
continue
}
config, err := conf.FromWgQuickWithUnknownEncoding(unparsedConfig.Config, unparsedConfig.Name)
if err != nil {
lastErr = err
continue
}
_, err = manager.IPCClientNewTunnel(config)
if err != nil {
lastErr = err
continue
}
configCount++
}
tp.listView.SetSuspendTunnelsUpdate(false)
m, n := configCount, len(unparsedConfigs)
switch {
case n == 1 && m != n:
syncedMsgBox(l18n.Sprintf("Error"), l18n.Sprintf("Unable to import configuration: %v", lastErr), walk.MsgBoxIconWarning)
case n == 1 && m == n:
// nothing
case m == n:
syncedMsgBox(l18n.Sprintf("Imported tunnels"), l18n.Sprintf("Imported %d tunnels", m), walk.MsgBoxIconInformation)
case m != n:
syncedMsgBox(l18n.Sprintf("Imported tunnels"), l18n.Sprintf("Imported %d of %d tunnels", m, n), walk.MsgBoxIconWarning)
}
}()
}
func (tp *TunnelsPage) exportTunnels(filePath string) {
writeFileWithOverwriteHandling(tp.Form(), filePath, func(file *os.File) error {
writer := zip.NewWriter(file)
for _, tunnel := range tp.listView.model.tunnels {
cfg, err := tunnel.StoredConfig()
if err != nil {
return fmt.Errorf("onExportTunnels: tunnel.StoredConfig failed: %w", err)
}
w, err := writer.Create(tunnel.Name + ".conf")
if err != nil {
return fmt.Errorf("onExportTunnels: writer.Create failed: %w", err)
}
if _, err := w.Write(([]byte)(cfg.ToWgQuick())); err != nil {
return fmt.Errorf("onExportTunnels: cfg.ToWgQuick failed: %w", err)
}
}
return writer.Close()
})
}
func (tp *TunnelsPage) addTunnel(config *conf.Config) {
_, err := manager.IPCClientNewTunnel(config)
if err != nil {
showErrorCustom(tp.Form(), l18n.Sprintf("Unable to create tunnel"), err.Error())
}
}
// Handlers
func (tp *TunnelsPage) onTunnelsViewItemActivated() {
go func() {
globalState, err := manager.IPCClientGlobalState()
if err != nil || (globalState != manager.TunnelStarted && globalState != manager.TunnelStopped) {
return
}
oldState, err := tp.listView.CurrentTunnel().Toggle()
if err != nil {
tp.Synchronize(func() {
if oldState == manager.TunnelUnknown {
showErrorCustom(tp.Form(), l18n.Sprintf("Failed to determine tunnel state"), err.Error())
} else if oldState == manager.TunnelStopped {
showErrorCustom(tp.Form(), l18n.Sprintf("Failed to activate tunnel"), err.Error())
} else if oldState == manager.TunnelStarted {
showErrorCustom(tp.Form(), l18n.Sprintf("Failed to deactivate tunnel"), err.Error())
}
})
return
}
}()
}
func (tp *TunnelsPage) onEditTunnel() {
tunnel := tp.listView.CurrentTunnel()
if tunnel == nil {
return
}
if config := runEditDialog(tp.Form(), tunnel); config != nil {
go func() {
priorState, err := tunnel.State()
tunnel.Delete()
tunnel.WaitForStop()
tunnel, err2 := manager.IPCClientNewTunnel(config)
if err == nil && err2 == nil && (priorState == manager.TunnelStarting || priorState == manager.TunnelStarted) {
tunnel.Start()
}
}()
}
}
func (tp *TunnelsPage) onAddTunnel() {
if config := runEditDialog(tp.Form(), nil); config != nil {
// Save new
tp.addTunnel(config)
}
}
func (tp *TunnelsPage) onDelete() {
indices := tp.listView.SelectedIndexes()
if len(indices) == 0 {
return
}
var title, question string
if len(indices) > 1 {
tunnelCount := len(indices)
title = l18n.Sprintf("Delete %d tunnels", tunnelCount)
question = l18n.Sprintf("Are you sure you would like to delete %d tunnels?", tunnelCount)
} else {
tunnelName := tp.listView.model.tunnels[indices[0]].Name
title = l18n.Sprintf("Delete tunnel %s", tunnelName)
question = l18n.Sprintf("Are you sure you would like to delete tunnel %s?", tunnelName)
}
if walk.DlgCmdNo == walk.MsgBox(
tp.Form(),
title,
l18n.Sprintf("%s You cannot undo this action.", question),
walk.MsgBoxYesNo|walk.MsgBoxIconWarning) {
return
}
selectTunnelAfter := ""
if len(indices) < len(tp.listView.model.tunnels) {
sort.Ints(indices)
max := 0
for i, idx := range indices {
if idx+1 < len(tp.listView.model.tunnels) && (i+1 == len(indices) || idx+1 != indices[i+1]) {
max = idx + 1
} else if idx-1 >= 0 && (i == 0 || idx-1 != indices[i-1]) {
max = idx - 1
}
}
selectTunnelAfter = tp.listView.model.tunnels[max].Name
}
if len(selectTunnelAfter) > 0 {
tp.listView.selectTunnel(selectTunnelAfter)
}
tunnelsToDelete := make([]manager.Tunnel, len(indices))
for i, j := range indices {
tunnelsToDelete[i] = tp.listView.model.tunnels[j]
}
go func() {
tp.listView.SetSuspendTunnelsUpdate(true)
var errors []error
for _, tunnel := range tunnelsToDelete {
err := tunnel.Delete()
if err != nil && (len(errors) == 0 || errors[len(errors)-1].Error() != err.Error()) {
errors = append(errors, err)
}
}
tp.listView.SetSuspendTunnelsUpdate(false)
if len(errors) > 0 {
tp.listView.Synchronize(func() {
if len(errors) == 1 {
showErrorCustom(tp.Form(), l18n.Sprintf("Unable to delete tunnel"), l18n.Sprintf("A tunnel was unable to be removed: %s", errors[0].Error()))
} else {
showErrorCustom(tp.Form(), l18n.Sprintf("Unable to delete tunnels"), l18n.Sprintf("%d tunnels were unable to be removed.", len(errors)))
}
})
}
}()
}
func (tp *TunnelsPage) onSelectAll() {
tp.listView.SetSelectedIndexes([]int{-1})
}
func (tp *TunnelsPage) onImport() {
dlg := walk.FileDialog{
Filter: l18n.Sprintf("Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*"),
Title: l18n.Sprintf("Import tunnel(s) from file"),
}
if ok, _ := dlg.ShowOpenMultiple(tp.Form()); !ok {
return
}
tp.importFiles(dlg.FilePaths)
}
func (tp *TunnelsPage) onExportTunnels() {
dlg := walk.FileDialog{
Filter: l18n.Sprintf("Configuration ZIP Files (*.zip)|*.zip"),
Title: l18n.Sprintf("Export tunnels to zip"),
}
if ok, _ := dlg.ShowSave(tp.Form()); !ok {
return
}
if !strings.HasSuffix(dlg.FilePath, ".zip") {
dlg.FilePath += ".zip"
}
tp.exportTunnels(dlg.FilePath)
}
func (tp *TunnelsPage) swapFiller(enabled bool) bool {
if tp.fillerContainer.Visible() == enabled {
return enabled
}
tp.SetSuspended(true)
tp.fillerContainer.SetVisible(enabled)
tp.currentTunnelContainer.SetVisible(!enabled)
tp.SetSuspended(false)
return enabled
}
func (tp *TunnelsPage) onTunnelsChanged() {
if tp.swapFiller(tp.listView.model.RowCount() == 0) {
tp.fillerButton.SetText(l18n.Sprintf("Import tunnel(s) from file"))
tp.fillerHandler = tp.onImport
}
}
func (tp *TunnelsPage) onSelectedTunnelsChanged() {
if tp.listView.model.RowCount() == 0 {
return
}
indices := tp.listView.SelectedIndexes()
tunnelCount := len(indices)
if tp.swapFiller(tunnelCount > 1) {
tp.fillerButton.SetText(l18n.Sprintf("Delete %d tunnels", tunnelCount))
tp.fillerHandler = tp.onDelete
}
}

133
ui/ui.go
View File

@@ -1,133 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"fmt"
"runtime"
"runtime/debug"
"time"
"github.com/lxn/walk"
"github.com/lxn/win"
"golang.org/x/sys/windows"
"github.com/amnezia-vpn/awg-windows/l18n"
"github.com/amnezia-vpn/awg-windows/version"
"golang.zx2c4.com/wireguard/windows/manager"
)
var (
noTrayAvailable = false
shouldQuitManagerWhenExiting = false
startTime = time.Now()
IsAdmin = false // A global, because this really is global for the process
)
func RunUI() {
runtime.LockOSThread()
windows.SetProcessPriorityBoost(windows.CurrentProcess(), false)
defer func() {
if err := recover(); err != nil {
showErrorCustom(nil, "Panic", fmt.Sprint(err, "\n\n", string(debug.Stack())))
panic(err)
}
}()
var (
err error
mtw *ManageTunnelsWindow
tray *Tray
)
for mtw == nil {
mtw, err = NewManageTunnelsWindow()
if err != nil {
time.Sleep(time.Millisecond * 400)
}
}
for tray == nil {
tray, err = NewTray(mtw)
if err != nil {
if version.OsIsCore() {
noTrayAvailable = true
break
}
time.Sleep(time.Millisecond * 400)
}
}
manager.IPCClientRegisterManagerStopping(func() {
mtw.Synchronize(func() {
walk.App().Exit(0)
})
})
onUpdateNotification := func(updateState manager.UpdateState) {
if updateState == manager.UpdateStateUnknown {
return
}
mtw.Synchronize(func() {
switch updateState {
case manager.UpdateStateFoundUpdate:
mtw.UpdateFound()
if tray != nil && IsAdmin {
tray.UpdateFound()
}
case manager.UpdateStateUpdatesDisabledUnofficialBuild:
mtw.SetTitle(l18n.Sprintf("%s (unsigned build, no updates)", mtw.Title()))
}
})
}
manager.IPCClientRegisterUpdateFound(onUpdateNotification)
go func() {
updateState, err := manager.IPCClientUpdateState()
if err == nil {
onUpdateNotification(updateState)
}
}()
if tray == nil {
win.ShowWindow(mtw.Handle(), win.SW_MINIMIZE)
}
mtw.Run()
if tray != nil {
tray.Dispose()
}
mtw.Dispose()
if shouldQuitManagerWhenExiting {
_, err := manager.IPCClientQuit(true)
if err != nil {
showErrorCustom(nil, l18n.Sprintf("Error Exiting WireGuard"), l18n.Sprintf("Unable to exit service due to: %v. You may want to stop WireGuard from the service manager.", err))
}
}
}
func onQuit() {
shouldQuitManagerWhenExiting = true
walk.App().Exit(0)
}
func showError(err error, owner walk.Form) bool {
if err == nil {
return false
}
showErrorCustom(owner, l18n.Sprintf("Error"), err.Error())
return true
}
func showErrorCustom(owner walk.Form, title, message string) {
walk.MsgBox(owner, title, message, walk.MsgBoxIconError)
}
func showWarningCustom(owner walk.Form, title, message string) {
walk.MsgBox(owner, title, message, walk.MsgBoxIconWarning)
}

View File

@@ -1,142 +0,0 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.
*/
package ui
import (
"github.com/lxn/walk"
"github.com/amnezia-vpn/awg-windows/l18n"
"golang.zx2c4.com/wireguard/windows/manager"
"golang.zx2c4.com/wireguard/windows/updater"
)
type UpdatePage struct {
*walk.TabPage
}
func NewUpdatePage() (*UpdatePage, error) {
var err error
var disposables walk.Disposables
defer disposables.Treat()
up := &UpdatePage{}
if up.TabPage, err = walk.NewTabPage(); err != nil {
return nil, err
}
disposables.Add(up)
up.SetTitle(l18n.Sprintf("An Update is Available!"))
tabIcon, _ := loadShieldIcon(16)
up.SetImage(tabIcon)
up.SetLayout(walk.NewVBoxLayout())
instructions, err := walk.NewTextLabel(up)
if err != nil {
return nil, err
}
instructions.SetText(l18n.Sprintf("An update to WireGuard is available. It is highly advisable to update without delay."))
instructions.SetMinMaxSize(walk.Size{1, 0}, walk.Size{0, 0})
status, err := walk.NewTextLabel(up)
if err != nil {
return nil, err
}
status.SetText(l18n.Sprintf("Status: Waiting for user"))
status.SetMinMaxSize(walk.Size{1, 0}, walk.Size{0, 0})
bar, err := walk.NewProgressBar(up)
if err != nil {
return nil, err
}
bar.SetVisible(false)
button, err := walk.NewPushButton(up)
if err != nil {
return nil, err
}
updateIcon, _ := loadSystemIcon("shell32", -47, 32)
button.SetImage(updateIcon)
button.SetText(l18n.Sprintf("Update Now"))
if !IsAdmin {
button.SetText(l18n.Sprintf("Please ask the system administrator to update."))
button.SetEnabled(false)
status.SetText(l18n.Sprintf("Status: Waiting for administrator"))
}
walk.NewVSpacer(up)
switchToUpdatingState := func() {
if !bar.Visible() {
up.SetSuspended(true)
button.SetEnabled(false)
button.SetVisible(false)
bar.SetVisible(true)
bar.SetMarqueeMode(true)
up.SetSuspended(false)
status.SetText(l18n.Sprintf("Status: Waiting for updater service"))
}
}
switchToReadyState := func() {
if bar.Visible() {
up.SetSuspended(true)
bar.SetVisible(false)
bar.SetValue(0)
bar.SetRange(0, 1)
bar.SetMarqueeMode(false)
button.SetVisible(true)
button.SetEnabled(true)
up.SetSuspended(false)
}
}
button.Clicked().Attach(func() {
switchToUpdatingState()
err := manager.IPCClientUpdate()
if err != nil {
switchToReadyState()
status.SetText(l18n.Sprintf("Error: %v. Please try again.", err))
}
})
manager.IPCClientRegisterUpdateProgress(func(dp updater.DownloadProgress) {
up.Synchronize(func() {
switchToUpdatingState()
if dp.Error != nil {
switchToReadyState()
err := dp.Error
status.SetText(l18n.Sprintf("Error: %v. Please try again.", err))
return
}
if len(dp.Activity) > 0 {
stateText := dp.Activity
status.SetText(l18n.Sprintf("Status: %s", stateText))
}
if dp.BytesTotal > 0 {
bar.SetMarqueeMode(false)
bar.SetRange(0, int(dp.BytesTotal))
bar.SetValue(int(dp.BytesDownloaded))
} else {
bar.SetMarqueeMode(true)
bar.SetValue(0)
bar.SetRange(0, 1)
}
if dp.Complete {
switchToReadyState()
status.SetText(l18n.Sprintf("Status: Complete!"))
return
}
})
})
disposables.Spare()
return up, nil
}