From c097c311354ed31f9a69e28c10c08fe3cef57344 Mon Sep 17 00:00:00 2001 From: Jason Barnett Date: Thu, 11 Jan 2024 17:36:12 -0700 Subject: [PATCH] wgengine/router: add ip rules for unifi udm-pro Fixes: #4038 Signed-off-by: Jason Barnett --- version/distro/distro.go | 27 ++++++++++++++++ wgengine/router/router_linux.go | 46 ++++++++++++++++++++++++---- wgengine/router/router_linux_test.go | 36 ++++++++++++++++++++++ 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/version/distro/distro.go b/version/distro/distro.go index 8865a834b..449581222 100644 --- a/version/distro/distro.go +++ b/version/distro/distro.go @@ -10,6 +10,7 @@ import ( "os" "runtime" "strconv" + "strings" "tailscale.com/types/lazy" "tailscale.com/util/lineread" @@ -31,6 +32,7 @@ const ( WDMyCloud = Distro("wdmycloud") Unraid = Distro("unraid") Alpine = Distro("alpine") + UDMPro = Distro("udmpro") ) var distro lazy.SyncValue[Distro] @@ -76,6 +78,9 @@ func linuxDistro() Distro { case have("/usr/local/bin/freenas-debug"): // TrueNAS Scale runs on debian return TrueNAS + case isUDMPro(): + // UDM-Pro runs on debian + return UDMPro case have("/etc/debian_version"): return Debian case have("/etc/arch-release"): @@ -147,3 +152,25 @@ func DSMVersion() int { return v }) } + +func isUDMPro() bool { + if exists, err := fileContainsString("/etc/board.info", "UDMPRO"); err == nil && exists { + return true + } + if exists, err := fileContainsString("/etc/board.info", "Dream Machine PRO"); err == nil && exists { + return true + } + if exists, err := fileContainsString("/sys/firmware/devicetree/base/soc/board-cfg/id", "udm pro"); err == nil && exists { + return true + } + return false +} + +func fileContainsString(filePath, searchString string) (bool, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return false, err + } + + return strings.Contains(string(data), searchString), nil +} diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index e9c585d98..67e7fce36 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -1031,6 +1031,10 @@ func mustRouteTable(num int) RouteTable { var ( mainRouteTable = newRouteTable("main", 254) defaultRouteTable = newRouteTable("default", 253) + // Port 9 - WAN + udmProRouteTable1 = newRouteTable("201", 201) + // Port 10 - SFP+ WAN 2 + udmProRouteTable2 = newRouteTable("202", 202) // tailscaleRouteTable is the routing table number for Tailscale // network routes. See addIPRules for the detailed policy routing @@ -1051,7 +1055,7 @@ var ( tailscaleRouteTable = newRouteTable("tailscale", 52) ) -// ipRules are the policy routing rules that Tailscale uses. +// _ipRules are the policy routing rules that Tailscale uses. // The priority is the value represented here added to r.ipPolicyPrefBase, // which is usually 5200. // @@ -1066,7 +1070,7 @@ var ( // and 'ip rule' implementations (including busybox), don't support // checking for the lack of a fwmark, only the presence. The technique // below works even on very old kernels. -var ipRules = []netlink.Rule{ +var _ipRules = []netlink.Rule{ // Packets from us, tagged with our fwmark, first try the kernel's // main routing table. { @@ -1102,6 +1106,17 @@ var ipRules = []netlink.Rule{ // usual rules (pref 32766 and 32767, ie. main and default). } +var addedUDMProIPRules = false + +func ipRules() []netlink.Rule { + // lazy add UDM Pro rules + if distro.Get() == distro.UDMPro { + addUDMProIpRules() + } + + return _ipRules +} + // justAddIPRules adds policy routing rule without deleting any first. func (r *linuxRouter) justAddIPRules() error { if !r.ipRuleAvailable { @@ -1113,7 +1128,7 @@ func (r *linuxRouter) justAddIPRules() error { var errAcc error for _, family := range r.addrFamilies() { - for _, ru := range ipRules { + for _, ru := range ipRules() { // Note: r is a value type here; safe to mutate it. ru.Family = family.netlinkInt() if ru.Mark != 0 { @@ -1142,7 +1157,7 @@ func (r *linuxRouter) addIPRulesWithIPCommand() error { rg := newRunGroup(nil, r.cmd) for _, family := range r.addrFamilies() { - for _, rule := range ipRules { + for _, rule := range ipRules() { args := []string{ "ip", family.dashArg(), "rule", "add", @@ -1190,7 +1205,7 @@ func (r *linuxRouter) delIPRules() error { } var errAcc error for _, family := range r.addrFamilies() { - for _, ru := range ipRules { + for _, ru := range ipRules() { // Note: r is a value type here; safe to mutate it. // When deleting rules, we want to be a bit specific (mention which // table we were routing to) but not *too* specific (fwmarks, etc). @@ -1233,7 +1248,7 @@ func (r *linuxRouter) delIPRulesWithIPCommand() error { // That leaves us some flexibility to change these values in later // versions without having ongoing hacks for every possible // combination. - for _, rule := range ipRules { + for _, rule := range ipRules() { args := []string{ "ip", family.dashArg(), "rule", "del", @@ -1395,3 +1410,22 @@ func nlAddrOfPrefix(p netip.Prefix) *netlink.Addr { IPNet: netipx.PrefixIPNet(p), } } + +// Adds necessary ip rules for UDM-Pro. See issue 4038. +func addUDMProIpRules() { + if addedUDMProIPRules { + return + } + + _ipRules = append(_ipRules, netlink.Rule{ + Priority: 21, + Mark: linuxfw.TailscaleBypassMarkNum, + Table: udmProRouteTable1.Num, + }, netlink.Rule{ + Priority: 22, + Mark: linuxfw.TailscaleBypassMarkNum, + Table: udmProRouteTable2.Num, + }) + + addedUDMProIPRules = true +} diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go index b86469f6d..a20e3f565 100644 --- a/wgengine/router/router_linux_test.go +++ b/wgengine/router/router_linux_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + ts_netlink "github.com/tailscale/netlink" "github.com/tailscale/wireguard-go/tun" "github.com/vishvananda/netlink" "go4.org/netipx" @@ -1132,3 +1133,38 @@ func adjustFwmask(t *testing.T, s string) string { return fwmaskAdjustRe.ReplaceAllString(s, "$1") } + +func TestAddUDMProIpRules(t *testing.T) { + originalIpRules := make([]ts_netlink.Rule, len(_ipRules)) + copy(originalIpRules, _ipRules) + + addUDMProIpRules() + + expectedRules := []ts_netlink.Rule{ + { + Priority: 21, + Mark: linuxfw.TailscaleBypassMarkNum, + Table: udmProRouteTable1.Num, + }, + { + Priority: 22, + Mark: linuxfw.TailscaleBypassMarkNum, + Table: udmProRouteTable2.Num, + }, + } + + for _, expected := range expectedRules { + found := false + for _, actual := range ipRules() { + if reflect.DeepEqual(actual, expected) { + found = true + break + } + } + if !found { + t.Errorf("Expected rule %+v; not found in ipRules", expected) + } + } + + _ipRules = originalIpRules +}