From 578b6e15cb21691d69bf7660322f980e498177ae Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Tue, 16 Apr 2019 15:33:56 +0200 Subject: ui: refactor exclude private IPs code to be closer to existing ports and preserve user edits Signed-off-by: Alexander Neumann --- ui/tunnelconfigdialog.go | 305 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 220 insertions(+), 85 deletions(-) (limited to 'ui/tunnelconfigdialog.go') diff --git a/ui/tunnelconfigdialog.go b/ui/tunnelconfigdialog.go index 8c2fc274..0a7432ac 100644 --- a/ui/tunnelconfigdialog.go +++ b/ui/tunnelconfigdialog.go @@ -15,21 +15,44 @@ import ( "golang.zx2c4.com/wireguard/windows/ui/syntax" ) -const ipv4DefaultRouteString = "0.0.0.0/0" +const ( + configKeyDNS = "DNS" + configKeyAllowedIPs = "AllowedIPs" +) + +var ( + ipv4Wildcard = orderedStringSetFromSlice([]string{"0.0.0.0/0"}) + ipv4PublicNetworks = orderedStringSetFromSlice([]string{ + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", + "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", + "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", + "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", + "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", + "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4", + }) +) + +type allowedIPsState int + +const ( + allowedIPsStateInvalid allowedIPsState = iota + allowedIPsStateContainsIPV4Wildcard + allowedIPsStateContainsIPV4PublicNetworks + allowedIPsStateOther +) type TunnelConfigDialog struct { *walk.Dialog - nameEdit *walk.LineEdit - pubkeyEdit *walk.LineEdit - syntaxEdit *syntax.SyntaxEdit - excludePrivateIPsCB *walk.CheckBox - saveButton *walk.PushButton - tunnel *service.Tunnel - config conf.Config - ipv4DefaultRouteModRFC1918CIDRs []string - ipv4DefaultRouteModRFC1918String string - lastPrivateKey string - inCheckedChanged bool + nameEdit *walk.LineEdit + pubkeyEdit *walk.LineEdit + syntaxEdit *syntax.SyntaxEdit + excludePrivateIPsCB *walk.CheckBox + saveButton *walk.PushButton + tunnel *service.Tunnel + config conf.Config + allowedIPsState allowedIPsState + lastPrivateKey string + inCheckedChanged bool } func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config { @@ -43,7 +66,6 @@ func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config if tunnel == nil { // Creating a new tunnel, create a new private key and use the default template title = "Create new tunnel" - name = "New tunnel" pk, _ := conf.NewPrivateKey() dlg.config = conf.Config{Interface: conf.Interface{PrivateKey: *pk}} } else { @@ -61,19 +83,8 @@ func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config dlg.SetIcon(owner.Icon()) dlg.SetTitle(title) dlg.SetLayout(layout) - // TODO: use size hints in layout elements to communicate the minimal width dlg.SetMinMaxSize(walk.Size{500, 400}, walk.Size{9999, 9999}) - dlg.ipv4DefaultRouteModRFC1918CIDRs = []string{ // Set of all non-private IPv4 IPs - "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", - "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", - "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", - "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", - "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", - "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4", - } - dlg.ipv4DefaultRouteModRFC1918String = strings.Join(dlg.ipv4DefaultRouteModRFC1918CIDRs, ", ") - nameLabel, _ := walk.NewTextLabel(dlg) layout.SetRange(nameLabel, walk.Rectangle{0, 0, 1, 1}) nameLabel.SetTextAlignment(walk.AlignHFarVCenter) @@ -81,7 +92,6 @@ func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config dlg.nameEdit, _ = walk.NewLineEdit(dlg) layout.SetRange(dlg.nameEdit, walk.Rectangle{1, 0, 1, 1}) - // TODO: compute the next available tunnel name ? dlg.nameEdit.SetText(name) pubkeyLabel, _ := walk.NewTextLabel(dlg) @@ -107,7 +117,6 @@ func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config dlg.excludePrivateIPsCB, _ = walk.NewCheckBox(buttonsContainer) dlg.excludePrivateIPsCB.SetText("Exclude private IPs") - dlg.excludePrivateIPsCB.SetChecked(dlg.privateIPsExcluded()) dlg.excludePrivateIPsCB.CheckedChanged().Attach(dlg.onExcludePrivateIPsCBCheckedChanged) dlg.updateExcludePrivateIPsCBVisible() @@ -124,6 +133,8 @@ func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config dlg.SetCancelButton(cancelButton) dlg.SetDefaultButton(dlg.saveButton) + dlg.updateAllowedIPsState() + if dlg.Run() == walk.DlgCmdOK { // Save return &dlg.config @@ -132,72 +143,132 @@ func runTunnelConfigDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config return nil } -func (dlg *TunnelConfigDialog) updateExcludePrivateIPsCBVisible() { - if dlg.inCheckedChanged { - return +func (dlg *TunnelConfigDialog) updateAllowedIPsState() { + var newState allowedIPsState + if len(dlg.config.Peers) == 1 { + if allowedIPs := dlg.allowedIPsSet(); allowedIPs.IsSupersetOf(ipv4Wildcard) { + newState = allowedIPsStateContainsIPV4Wildcard + } else if allowedIPs.IsSupersetOf(ipv4PublicNetworks) { + newState = allowedIPsStateContainsIPV4PublicNetworks + } else { + newState = allowedIPsStateOther + } + } else { + newState = allowedIPsStateInvalid } - visible := len(dlg.config.Peers) == 1 - - if visible { - cidrsSlice := dlg.allowedCIDRsSlice() - cidrsSet := dlg.setFromSlice(cidrsSlice) + if newState != dlg.allowedIPsState { + dlg.allowedIPsState = newState - if len(cidrsSet) != 1 || !cidrsSet[ipv4DefaultRouteString] { - for _, cidr := range dlg.ipv4DefaultRouteModRFC1918CIDRs { - if !cidrsSet[cidr] { - visible = false - break - } - } - } + dlg.excludePrivateIPsCB.SetVisible(dlg.canExcludePrivateIPs()) + dlg.excludePrivateIPsCB.SetChecked(dlg.privateIPsExcluded()) } +} - dlg.excludePrivateIPsCB.SetVisible(visible) +func (dlg *TunnelConfigDialog) canExcludePrivateIPs() bool { + return dlg.allowedIPsState == allowedIPsStateContainsIPV4PublicNetworks || + dlg.allowedIPsState == allowedIPsStateContainsIPV4Wildcard } -func (dlg *TunnelConfigDialog) allowedCIDRsSlice() []string { - var cidrs []string +func (dlg *TunnelConfigDialog) privateIPsExcluded() bool { + return dlg.allowedIPsState == allowedIPsStateContainsIPV4PublicNetworks +} - lines := strings.Split(dlg.syntaxEdit.Text(), "\n") - for _, line := range lines { - if strings.Contains(line, "AllowedIPs") { - cidrsMaybeWithSpace := strings.Split(strings.TrimSpace(line[strings.IndexByte(line, '=')+1:]), ",") - cidrs = make([]string, len(cidrsMaybeWithSpace)) - for i, cidr := range cidrsMaybeWithSpace { - cidrs[i] = strings.TrimSpace(cidr) +func (dlg *TunnelConfigDialog) setPrivateIPsExcluded(excluded bool) { + if !dlg.canExcludePrivateIPs() || dlg.privateIPsExcluded() == excluded { + return + } + + var oldNetworks, newNetworks *orderedStringSet + if excluded { + oldNetworks, newNetworks = ipv4Wildcard, ipv4PublicNetworks + } else { + oldNetworks, newNetworks = ipv4PublicNetworks, ipv4Wildcard + } + input := dlg.allowedIPs() + output := newOrderedStringSet() + var replaced bool + + // Replace the first instance of the wildcard with the public network list, or vice versa. + for _, network := range input { + if oldNetworks.Contains(network) { + if !replaced { + output.UniteWith(newNetworks) + replaced = true } - break + } else { + output.Add(network) } } - return cidrs + // DNS servers only need to be handled specially when we're excluding private IPs. + for _, route := range dlg.dnsRoutes() { + if excluded { + output.Add(route) + } else { + output.Remove(route) + output.Remove(route + "/32") + } + } + + if excluded { + dlg.allowedIPsState = allowedIPsStateContainsIPV4PublicNetworks + } else { + dlg.allowedIPsState = allowedIPsStateContainsIPV4Wildcard + } + + dlg.replaceLine(configKeyAllowedIPs, strings.Join(output.ToSlice(), ", ")) } -func (dlg *TunnelConfigDialog) setFromSlice(slice []string) map[string]bool { - set := make(map[string]bool) +func (dlg *TunnelConfigDialog) replaceLine(key, value string) { + text := dlg.syntaxEdit.Text() - for _, s := range slice { - set[s] = true - } + start := strings.Index(text, key) + end := start + strings.Index(text[start:], "\n") + oldLine := text[start:end] + newLine := fmt.Sprintf("%s = %s", key, value) - return set + dlg.syntaxEdit.SetText(strings.ReplaceAll(text, oldLine, newLine)) } -func (dlg *TunnelConfigDialog) privateIPsExcluded() bool { - allowedCIDRs := dlg.setFromSlice(dlg.allowedCIDRsSlice()) +func (dlg *TunnelConfigDialog) updateExcludePrivateIPsCBVisible() { + dlg.updateAllowedIPsState() - if len(allowedCIDRs) != len(dlg.ipv4DefaultRouteModRFC1918CIDRs) { - return false - } + dlg.excludePrivateIPsCB.SetVisible(dlg.canExcludePrivateIPs()) +} - for _, cidr := range dlg.ipv4DefaultRouteModRFC1918CIDRs { - if !allowedCIDRs[cidr] { - return false +func (dlg *TunnelConfigDialog) dnsRoutes() []string { + return dlg.routes(configKeyDNS) +} + +func (dlg *TunnelConfigDialog) allowedIPs() []string { + return dlg.routes(configKeyAllowedIPs) +} + +func (dlg *TunnelConfigDialog) allowedIPsSet() *orderedStringSet { + return orderedStringSetFromSlice(dlg.allowedIPs()) +} + +func (dlg *TunnelConfigDialog) routes(key string) []string { + var routes []string + + lines := strings.Split(dlg.syntaxEdit.Text(), "\n") + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), key) { + routesMaybeWithSpace := strings.Split(strings.TrimSpace(line[strings.IndexByte(line, '=')+1:]), ",") + routes = make([]string, len(routesMaybeWithSpace)) + for i, route := range routesMaybeWithSpace { + routes[i] = strings.TrimSpace(route) + } + break } } - return true + return routes +} + +func (dlg *TunnelConfigDialog) onExcludePrivateIPsCBCheckedChanged() { + dlg.setPrivateIPsExcluded(dlg.excludePrivateIPsCB.Checked()) } func (dlg *TunnelConfigDialog) onSyntaxEditPrivateKeyChanged(privateKey string) { @@ -213,22 +284,6 @@ func (dlg *TunnelConfigDialog) onSyntaxEditPrivateKeyChanged(privateKey string) } } -func (dlg *TunnelConfigDialog) onExcludePrivateIPsCBCheckedChanged() { - dlg.inCheckedChanged = true - defer func() { - dlg.inCheckedChanged = false - }() - - var before, after string - if dlg.excludePrivateIPsCB.Checked() { - before, after = ipv4DefaultRouteString, dlg.ipv4DefaultRouteModRFC1918String - } else { - before, after = dlg.ipv4DefaultRouteModRFC1918String, ipv4DefaultRouteString - } - // TODO: Preserve changes the user may have done to the list? - dlg.syntaxEdit.SetText(strings.ReplaceAll(dlg.syntaxEdit.Text(), "AllowedIPs = "+before, "AllowedIPs = "+after)) -} - func (dlg *TunnelConfigDialog) onSaveButtonClicked() { newName := dlg.nameEdit.Text() if newName == "" { @@ -266,3 +321,83 @@ func (dlg *TunnelConfigDialog) onSaveButtonClicked() { dlg.Accept() } + +type orderedStringSet struct { + items []string + item2index map[string]int +} + +func orderedStringSetFromSlice(items []string) *orderedStringSet { + oss := newOrderedStringSet() + oss.AddMany(items) + return oss +} + +func newOrderedStringSet() *orderedStringSet { + return &orderedStringSet{item2index: make(map[string]int)} +} + +func (oss *orderedStringSet) Add(item string) bool { + if _, ok := oss.item2index[item]; ok { + return false + } + + oss.item2index[item] = len(oss.items) + oss.items = append(oss.items, item) + return true +} + +func (oss *orderedStringSet) AddMany(items []string) { + for _, item := range items { + oss.Add(item) + } +} + +func (oss *orderedStringSet) UniteWith(other *orderedStringSet) { + if other == oss { + return + } + + oss.AddMany(other.items) +} + +func (oss *orderedStringSet) Remove(item string) bool { + if i, ok := oss.item2index[item]; ok { + oss.items = append(oss.items[:i], oss.items[i+1:]...) + delete(oss.item2index, item) + return true + } + + return false +} + +func (oss *orderedStringSet) Len() int { + return len(oss.items) +} + +func (oss *orderedStringSet) ToSlice() []string { + return append(([]string)(nil), oss.items...) +} + +func (oss *orderedStringSet) Contains(item string) bool { + _, ok := oss.item2index[item] + return ok +} + +func (oss *orderedStringSet) IsSupersetOf(other *orderedStringSet) bool { + if oss.Len() < other.Len() { + return false + } + + for _, item := range other.items { + if !oss.Contains(item) { + return false + } + } + + return true +} + +func (oss *orderedStringSet) String() string { + return fmt.Sprintf("%v", oss.items) +} -- cgit v1.2.3-59-g8ed1b