Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/ui: rework Mullvad peer list to be hierarchical #142

Merged
merged 11 commits into from
Jul 12, 2024
1 change: 1 addition & 0 deletions dev.deedles.Trayscale.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<release version="v0.13.0" date="TODO">
<description>
<ul>Remove control server dconf setting and instead use a new dialog.</ul>
<ul>Make Mullvad peer list hierarchical.</ul>
</description>
</release>
<release version="v0.12.7" date="2024-07-09">
Expand Down
19 changes: 10 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ go 1.22.5
require (
deedles.dev/mk v0.1.0
fyne.io/systray v1.11.0
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240512000724-8dc7455ee58f
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240712143708-824c3ce8a5f4
github.com/diamondburned/gotk4/pkg v0.2.2
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
golang.org/x/net v0.26.0
golang.org/x/net v0.27.0
gotest.tools/v3 v3.5.1
honnef.co/go/tools v0.4.7
tailscale.com v1.68.2
)
Expand All @@ -20,7 +21,7 @@ require (
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect
github.com/dblohm7/wingoes v0.0.0-20240705145628-15336bf25109 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
Expand Down Expand Up @@ -53,16 +54,16 @@ require (
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/exp/typeparams v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/exp/typeparams v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
k8s.io/client-go v0.30.2 // indirect
nhooyr.io/websocket v1.8.11 // indirect
Expand Down
38 changes: 20 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 h1:vrC07UZcgPzu/OjWsmQKMGg3LoPSz9jh/pQXIrHjUj4=
github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240512000724-8dc7455ee58f h1:1BGSLL+wWJjnMzRkRNR8oR5joi6gxyUSD4cI+L67wCc=
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240512000724-8dc7455ee58f/go.mod h1:fkvdR7MYO1sI0ex07VYLTc+YK87v24aRFYyMJQ/xAeA=
github.com/dblohm7/wingoes v0.0.0-20240705145628-15336bf25109 h1:48mQmwgqKcAkXWafKN1jI4nN7LKfK/EivFQSU/wKa0Q=
github.com/dblohm7/wingoes v0.0.0-20240705145628-15336bf25109/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240712143708-824c3ce8a5f4 h1:LIOh9NaVui4TaCLbWHe3Yn/7liGdWgH2LsUp+xhTqkw=
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20240712143708-824c3ce8a5f4/go.mod h1:fkvdR7MYO1sI0ex07VYLTc+YK87v24aRFYyMJQ/xAeA=
github.com/diamondburned/gotk4/pkg v0.2.2 h1:6EBtg/7uhnN/sCJd7rZdc+PqMXmnEaEDNIC9K/hTZfQ=
github.com/diamondburned/gotk4/pkg v0.2.2/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
Expand Down Expand Up @@ -107,16 +107,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/exp/typeparams v0.0.0-20240613232115-7f521ea00fb8 h1:+ZJmEdDFzH5H0CnzOrwgbH3elHctfTecW9X0k2tkn5M=
golang.org/x/exp/typeparams v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/typeparams v0.0.0-20240707233637-46b078467d37 h1:sLjLh33O815/196VSJe2X7Mmaud/GGjSubpzkgfRroY=
golang.org/x/exp/typeparams v0.0.0-20240707233637-46b078467d37/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -128,18 +128,20 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
Expand Down
14 changes: 9 additions & 5 deletions internal/tsutil/tsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,18 @@ func CompareLocations(loc1, loc2 *tailcfg.Location) int {
}

// ComparePeers compares two peers. It does so by location if
// available or hostname if not. It returns the peers in a
// available, then by hostname. It returns the peers in a
// deterministic order if their locations or hostnames are identical,
// so the result of calling this is never 0. To determine if peers are
// the same, compare their IDs manually.
func ComparePeers(p1, p2 *ipnstate.PeerStatus) int {
ids := cmp.Compare(p1.ID, p2.ID)
if (p1.Location == nil) || (p2.Location == nil) {
return cmp.Or(cmp.Compare(p1.HostName, p2.HostName), ids)
loc := 0
if p1.Location != nil && p2.Location != nil {
loc = CompareLocations(p1.Location, p2.Location)
}
return cmp.Or(CompareLocations(p1.Location, p2.Location), ids)
return cmp.Or(
loc,
cmp.Compare(p1.HostName, p2.HostName),
cmp.Compare(p1.ID, p2.ID),
)
}
139 changes: 92 additions & 47 deletions internal/ui/mullvadpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"slices"

"deedles.dev/trayscale/internal/tsutil"
"deedles.dev/trayscale/internal/xslices"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"tailscale.com/ipn/ipnstate"
Expand All @@ -26,7 +27,12 @@ type MullvadPage struct {

name string

exitNodeRows rowManager[*ipnstate.PeerStatus]
nodeLocationRows rowManager[[]*ipnstate.PeerStatus]

// These are used to cache some intermediate variables between
// updates to cut down on the number of necessary allocations.
nodes []*ipnstate.PeerStatus
locs [][]*ipnstate.PeerStatus
}

func NewMullvadPage(a *App, status tsutil.Status) *MullvadPage {
Expand All @@ -51,46 +57,54 @@ func (page *MullvadPage) Name() string {
func (page *MullvadPage) init(a *App, status tsutil.Status) {
page.name = mullvadPageBaseName

page.exitNodeRows.Parent = page.ExitNodesGroup
page.exitNodeRows.New = func(peer *ipnstate.PeerStatus) row[*ipnstate.PeerStatus] {
row := exitNodeRow{
peer: peer,

w: adw.NewSwitchRow(),
page.nodeLocationRows.Parent = page.ExitNodesGroup
page.nodeLocationRows.New = func(peers []*ipnstate.PeerStatus) row[[]*ipnstate.PeerStatus] {
r := nodeLocationRow{
w: adw.NewExpanderRow(),
}
r.m.Parent = rowAdderParent{r.w}
r.m.New = func(peer *ipnstate.PeerStatus) row[*ipnstate.PeerStatus] {
row := exitNodeRow{
peer: peer,

row.w.SetTitle(mullvadExitNodeName(peer))

row.r().SetMarginTop(12)
row.r().SetMarginBottom(12)
row.r().ConnectStateSet(func(s bool) bool {
if s == row.r().State() {
return false
w: adw.NewSwitchRow(),
}

if s {
err := tsutil.AdvertiseExitNode(context.TODO(), false)
if err != nil {
slog.Error("disable exit node advertisement", "err", err)
// Continue anyways.
row.w.SetTitle(peer.HostName)

row.r().SetMarginTop(12)
row.r().SetMarginBottom(12)
row.r().ConnectStateSet(func(s bool) bool {
if s == row.r().State() {
return false
}
}

var node *ipnstate.PeerStatus
if s {
node = row.peer
}
err := tsutil.ExitNode(context.TODO(), node)
if err != nil {
slog.Error("set exit node", "err", err)
row.r().SetActive(!s)
if s {
err := tsutil.AdvertiseExitNode(context.TODO(), false)
if err != nil {
slog.Error("disable exit node advertisement", "err", err)
// Continue anyways.
}
}

var node *ipnstate.PeerStatus
if s {
node = row.peer
}
err := tsutil.ExitNode(context.TODO(), node)
if err != nil {
slog.Error("set exit node", "err", err)
row.r().SetActive(!s)
return true
}
a.poller.Poll() <- struct{}{}
return true
}
a.poller.Poll() <- struct{}{}
return true
})
})

return &row
}

return &row
return &r
}
}

Expand All @@ -102,18 +116,53 @@ func (page *MullvadPage) Update(a *App, peer *ipnstate.PeerStatus, status tsutil
exitNodeID = status.Status.ExitNodeStatus.ID
}

nodes := make([]*ipnstate.PeerStatus, 0, len(status.Status.Peer))
for _, peer := range status.Status.Peer {
if tsutil.IsMullvad(peer) {
nodes = append(nodes, peer)
page.nodes = append(page.nodes, peer)
if peer.ID == exitNodeID {
page.name = fmt.Sprintf("%v [%v]", mullvadPageBaseName, mullvadExitNodeName(peer))
page.name = fmt.Sprintf("%v [%v]", mullvadPageBaseName, mullvadLocationName(peer.Location))
}
}
}
slices.SortFunc(nodes, tsutil.ComparePeers)
slices.SortFunc(page.nodes, tsutil.ComparePeers)

page.exitNodeRows.Update(nodes)
type locID struct {
CountryCode string
CityCode string
}
page.locs = page.locs[:0]
page.locs = xslices.AppendChunkBy(page.locs, page.nodes, func(peer *ipnstate.PeerStatus) locID {
return locID{peer.Location.CountryCode, peer.Location.CityCode}
})

page.nodeLocationRows.Update(page.locs)

clear(page.nodes)
page.nodes = page.nodes[:0]
}

type nodeLocationRow struct {
w *adw.ExpanderRow
m rowManager[*ipnstate.PeerStatus]
}

func (row *nodeLocationRow) Update(nodes []*ipnstate.PeerStatus) {
loc := nodes[0].Location

row.w.SetTitle(mullvadLocationName(loc))
row.w.SetSubtitle("")
for _, peer := range nodes {
if peer.ExitNode {
row.w.SetSubtitle("Current exit node location")
break
}
}

row.m.Update(nodes)
}

func (row *nodeLocationRow) Widget() gtk.Widgetter {
return row.w
}

type exitNodeRow struct {
Expand All @@ -129,7 +178,7 @@ func (row *exitNodeRow) r() *gtk.Switch {
func (row *exitNodeRow) Update(peer *ipnstate.PeerStatus) {
row.peer = peer

row.w.SetTitle(mullvadExitNodeName(peer))
row.w.SetTitle(peer.HostName)

row.r().SetState(peer.ExitNode)
row.r().SetActive(peer.ExitNode)
Expand All @@ -139,16 +188,12 @@ func (row *exitNodeRow) Widget() gtk.Widgetter {
return row.w
}

func mullvadExitNodeName(peer *ipnstate.PeerStatus) string {
if peer.Location == nil {
return peer.HostName
}

func mullvadLocationName(loc *tailcfg.Location) string {
return fmt.Sprintf(
"%v %v, %v",
countryCodeToFlag(peer.Location.CountryCode),
peer.Location.City,
peer.Location.Country,
countryCodeToFlag(loc.CountryCode),
loc.City,
loc.Country,
)
}

Expand Down
8 changes: 8 additions & 0 deletions internal/xmaps/xmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ func Entries[M ~map[K]V, K comparable, V any](m M) []Entry[K, V] {
}
return r
}

func Values[M ~map[K]V, K comparable, V any](m M) []V {
r := make([]V, 0, len(m))
for _, v := range m {
r = append(r, v)
}
return r
}
Loading
Loading