From bbdd3c3bdecdb9576040d1e2018f73df4ee339fc Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 1 Apr 2025 04:01:00 -0700 Subject: [PATCH] wgengine/router: add Plan 9 implementation Updates #5794 Change-Id: Ib78a3ea971a2374d405b024ab88658ec34be59a6 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 5 + wgengine/router/router_default.go | 2 +- wgengine/router/router_plan9.go | 156 ++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 wgengine/router/router_plan9.go diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c44827aa4..7d69b884d 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -5069,6 +5069,11 @@ func shouldUseOneCGNATRoute(logf logger.Logf, mon *netmon.Monitor, controlKnobs } } + if versionOS == "plan9" { + // Just temporarily during plan9 bringup to have fewer routes to debug. + return true + } + // Also prefer to do this on the Mac, so that we don't need to constantly // update the network extension configuration (which is disruptive to // Chrome, see https://github.com/tailscale/tailscale/issues/3102). Only diff --git a/wgengine/router/router_default.go b/wgengine/router/router_default.go index 1e675d1fc..8dcbd36d0 100644 --- a/wgengine/router/router_default.go +++ b/wgengine/router/router_default.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !windows && !linux && !darwin && !openbsd && !freebsd +//go:build !windows && !linux && !darwin && !openbsd && !freebsd && !plan9 package router diff --git a/wgengine/router/router_plan9.go b/wgengine/router/router_plan9.go new file mode 100644 index 000000000..7ed7686d9 --- /dev/null +++ b/wgengine/router/router_plan9.go @@ -0,0 +1,156 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package router + +import ( + "bufio" + "bytes" + "fmt" + "net/netip" + "os" + "strings" + + "github.com/tailscale/wireguard-go/tun" + "tailscale.com/health" + "tailscale.com/net/netmon" + "tailscale.com/types/logger" +) + +func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) { + r := &plan9Router{ + logf: logf, + tundev: tundev, + netMon: netMon, + } + cleanAllTailscaleRoutes(logf) + return r, nil +} + +type plan9Router struct { + logf logger.Logf + tundev tun.Device + netMon *netmon.Monitor + health *health.Tracker +} + +func (r *plan9Router) Up() error { + return nil +} + +func (r *plan9Router) Set(cfg *Config) error { + if cfg == nil { + cleanAllTailscaleRoutes(r.logf) + return nil + } + + var self4, self6 netip.Addr + for _, addr := range cfg.LocalAddrs { + ctl := r.tundev.File() + maskBits := addr.Bits() + if addr.Addr().Is4() { + // The mask sizes in Plan9 are in IPv6 bits, even for IPv4. + maskBits += (128 - 32) + self4 = addr.Addr() + } + if addr.Addr().Is6() { + self6 = addr.Addr() + } + _, err := fmt.Fprintf(ctl, "add %s /%d\n", addr.Addr().String(), maskBits) + r.logf("route/plan9: add %s /%d = %v", addr.Addr().String(), maskBits, err) + } + + ipr, err := os.OpenFile("/net/iproute", os.O_RDWR, 0) + if err != nil { + return fmt.Errorf("open /net/iproute: %w", err) + } + defer ipr.Close() + + // TODO(bradfitz): read existing routes, delete ones tagged "tail" + // that aren't in cfg.LocalRoutes. + + if _, err := fmt.Fprintf(ipr, "tag tail\n"); err != nil { + return fmt.Errorf("tag tail: %w", err) + } + + for _, route := range cfg.Routes { + maskBits := route.Bits() + if route.Addr().Is4() { + // The mask sizes in Plan9 are in IPv6 bits, even for IPv4. + maskBits += (128 - 32) + } + var nextHop netip.Addr + if route.Addr().Is4() { + nextHop = self4 + } else if route.Addr().Is6() { + nextHop = self6 + } + if !nextHop.IsValid() { + r.logf("route/plan9: skipping route %s: no next hop (no self addr)", route.String()) + continue + } + r.logf("route/plan9: plan9.router: add %s /%d %s", route.Addr(), maskBits, nextHop) + if _, err := fmt.Fprintf(ipr, "add %s /%d %s\n", route.Addr(), maskBits, nextHop); err != nil { + return fmt.Errorf("add %s: %w", route.String(), err) + } + } + + if len(cfg.LocalRoutes) > 0 { + r.logf("route/plan9: TODO: Set LocalRoutes %v", cfg.LocalRoutes) + } + if len(cfg.SubnetRoutes) > 0 { + r.logf("route/plan9: TODO: Set SubnetRoutes %v", cfg.SubnetRoutes) + } + + return nil +} + +// UpdateMagicsockPort implements the Router interface. This implementation +// does nothing and returns nil because this router does not currently need +// to know what the magicsock UDP port is. +func (r *plan9Router) UpdateMagicsockPort(_ uint16, _ string) error { + return nil +} + +func (r *plan9Router) Close() error { + // TODO(bradfitz): unbind + return nil +} + +func cleanUp(logf logger.Logf, _ string) { + cleanAllTailscaleRoutes(logf) +} + +func cleanAllTailscaleRoutes(logf logger.Logf) { + routes, err := os.OpenFile("/net/iproute", os.O_RDWR, 0) + if err != nil { + logf("cleaning routes: %v", err) + return + } + defer routes.Close() + + // Using io.ReadAll or os.ReadFile on /net/iproute fails; it results in a + // 511 byte result when the actual /net/iproute contents are over 1k. + // So do it in one big read instead. Who knows. + routeBuf := make([]byte, 1<<20) + n, err := routes.Read(routeBuf) + if err != nil { + logf("cleaning routes: %v", err) + return + } + routeBuf = routeBuf[:n] + + bs := bufio.NewScanner(bytes.NewReader(routeBuf)) + for bs.Scan() { + f := strings.Fields(bs.Text()) + if len(f) < 6 { + continue + } + tag := f[4] + if tag != "tail" { + continue + } + _, err := fmt.Fprintf(routes, "remove %s %s\n", f[0], f[1]) + logf("router: cleaning route %s %s: %v", f[0], f[1], err) + } +}