From ec52760a3d76be445cdc061cfbecc3540bbf8507 Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Wed, 16 Jun 2021 08:53:08 -0700 Subject: [PATCH] wgengine/router_windows: support toggling local lan access when using exit nodes. Signed-off-by: Maisem Ali --- cmd/tailscaled/tailscaled_windows.go | 32 +++++----- ipn/ipnlocal/local.go | 2 +- wgengine/router/router_windows.go | 87 ++++++++++++++++++---------- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index b52853ebe..277c4987d 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -19,6 +19,7 @@ package main // import "tailscale.com/cmd/tailscaled" import ( "context" + "encoding/json" "fmt" "log" "os" @@ -27,6 +28,7 @@ import ( "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" + "inet.af/netaddr" "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" "tailscale.com/net/dns" @@ -126,16 +128,6 @@ func beFirewallKillswitch() bool { log.SetFlags(0) log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2]) - go func() { - b := make([]byte, 16) - for { - _, err := os.Stdin.Read(b) - if err != nil { - log.Fatalf("parent process died or requested exit, exiting (%v)", err) - } - } - }() - guid, err := windows.GUIDFromString(os.Args[2]) if err != nil { log.Fatalf("invalid GUID %q: %v", os.Args[2], err) @@ -147,13 +139,25 @@ func beFirewallKillswitch() bool { } start := time.Now() - if _, err := wf.New(uint64(luid)); err != nil { - log.Fatalf("filewall creation failed: %v", err) + fw, err := wf.New(uint64(luid)) + if err != nil { + log.Fatalf("failed to enable firewall: %v", err) } log.Printf("killswitch enabled, took %s", time.Since(start)) - // Block until the monitor goroutine shuts us down. - select {} + // Note(maisem): when local lan access toggled, tailscaled needs to + // inform the firewall to let local routes through. The set of routes + // is passed in via stdin encoded in json. + dcd := json.NewDecoder(os.Stdin) + for { + var routes []netaddr.IPPrefix + if err := dcd.Decode(&routes); err != nil { + log.Fatalf("parent process died or requested exit, exiting (%v)", err) + } + if err := fw.UpdatePermittedRoutes(routes); err != nil { + log.Fatalf("failed to update routes (%v)", err) + } + } } func startIPNServer(ctx context.Context, logid string) error { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index fd777b0cb..60b6c4ae4 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2108,7 +2108,7 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router if !default6 { rs.Routes = append(rs.Routes, ipv6Default) } - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" { // Only allow local lan access on linux machines for now. ips, _, err := interfaceRoutes() if err != nil { diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go index e2d62792a..b107c6a3a 100644 --- a/wgengine/router/router_windows.go +++ b/wgengine/router/router_windows.go @@ -7,6 +7,7 @@ package router import ( "bufio" "context" + "encoding/json" "fmt" "io" "os" @@ -73,7 +74,7 @@ func (r *winRouter) Set(cfg *Config) error { for _, la := range cfg.LocalAddrs { localAddrs = append(localAddrs, la.String()) } - r.firewall.set(localAddrs, cfg.Routes) + r.firewall.set(localAddrs, cfg.Routes, cfg.LocalRoutes) err := configureInterface(cfg, r.nativeTun) if err != nil { @@ -121,12 +122,16 @@ type firewallTweaker struct { logf logger.Logf tunGUID windows.GUID - mu sync.Mutex - didProcRule bool - running bool // doAsyncSet goroutine is running - known bool // firewall is in known state (in lastVal) - wantLocal []string // next value we want, or "" to delete the firewall rule - lastLocal []string // last set value, if known + mu sync.Mutex + didProcRule bool + running bool // doAsyncSet goroutine is running + known bool // firewall is in known state (in lastVal) + wantLocal []string // next value we want, or "" to delete the firewall rule + lastLocal []string // last set value, if known + + localRoutes []netaddr.IPPrefix + lastLocalRoutes []netaddr.IPPrefix + wantKillswitch bool lastKillswitch bool @@ -138,16 +143,17 @@ type firewallTweaker struct { // non-nil any number of times during the process's lifetime. fwProc *exec.Cmd // stop makes fwProc exit when closed. - stop io.Closer + fwProcWriter io.WriteCloser + fwProcEncoder *json.Encoder } -func (ft *firewallTweaker) clear() { ft.set(nil, nil) } +func (ft *firewallTweaker) clear() { ft.set(nil, nil, nil) } // set takes CIDRs to allow, and the routes that point into the Tailscale tun interface. // Empty slices remove firewall rules. // // set takes ownership of cidrs, but not routes. -func (ft *firewallTweaker) set(cidrs []string, routes []netaddr.IPPrefix) { +func (ft *firewallTweaker) set(cidrs []string, routes, localRoutes []netaddr.IPPrefix) { ft.mu.Lock() defer ft.mu.Unlock() @@ -157,6 +163,7 @@ func (ft *firewallTweaker) set(cidrs []string, routes []netaddr.IPPrefix) { ft.logf("marking allowed %v", cidrs) } ft.wantLocal = cidrs + ft.localRoutes = localRoutes ft.wantKillswitch = hasDefaultRoute(routes) if ft.running { // The doAsyncSet goroutine will check ft.wantLocal/wantKillswitch @@ -184,7 +191,7 @@ func (ft *firewallTweaker) doAsyncSet() { ft.mu.Lock() for { // invariant: ft.mu must be locked when beginning this block val := ft.wantLocal - if ft.known && strsEqual(ft.lastLocal, val) && ft.wantKillswitch == ft.lastKillswitch { + if ft.known && strsEqual(ft.lastLocal, val) && ft.wantKillswitch == ft.lastKillswitch && routesEqual(ft.localRoutes, ft.lastLocalRoutes) { ft.running = false ft.logf("ending netsh goroutine") ft.mu.Unlock() @@ -193,13 +200,18 @@ func (ft *firewallTweaker) doAsyncSet() { wantKillswitch := ft.wantKillswitch needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0 needProcRule := !ft.didProcRule + localRoutes := ft.localRoutes ft.mu.Unlock() - err := ft.doSet(val, wantKillswitch, needClear, needProcRule) + err := ft.doSet(val, wantKillswitch, needClear, needProcRule, localRoutes) + if err != nil { + ft.logf("set failed: %v", err) + } bo.BackOff(ctx, err) ft.mu.Lock() ft.lastLocal = val + ft.lastLocalRoutes = localRoutes ft.lastKillswitch = wantKillswitch ft.known = (err == nil) } @@ -218,7 +230,7 @@ func (ft *firewallTweaker) doAsyncSet() { // process to dial out as it pleases. // // Must only be invoked from doAsyncSet. -func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, procRule bool) error { +func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, procRule bool, allowedRoutes []netaddr.IPPrefix) error { if clear { ft.logf("clearing Tailscale-In firewall rules...") // We ignore the error here, because netsh returns an error for @@ -271,24 +283,29 @@ func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, pr ft.logf("added Tailscale-In rule to allow %v in %v", cidr, d) } - if killswitch && ft.fwProc == nil { + if !killswitch { + if ft.fwProc != nil { + ft.fwProcWriter.Close() + ft.fwProcWriter = nil + ft.fwProc.Wait() + ft.fwProc = nil + ft.fwProcEncoder = nil + } + return nil + } + if ft.fwProc == nil { exe, err := os.Executable() if err != nil { return err } proc := exec.Command(exe, "/firewall", ft.tunGUID.String()) - var ( - out io.ReadCloser - in io.WriteCloser - ) - out, err = proc.StdoutPipe() + in, err := proc.StdinPipe() if err != nil { return err } - proc.Stderr = proc.Stdout - in, err = proc.StdinPipe() + out, err := proc.StdoutPipe() if err != nil { - out.Close() + in.Close() return err } @@ -305,20 +322,32 @@ func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, pr } } }(out) + proc.Stderr = proc.Stdout if err := proc.Start(); err != nil { return err } - ft.stop = in + ft.fwProcWriter = in ft.fwProc = proc - } else if !killswitch && ft.fwProc != nil { - ft.stop.Close() - ft.stop = nil - ft.fwProc.Wait() - ft.fwProc = nil + ft.fwProcEncoder = json.NewEncoder(in) } + // Note(maisem): when local lan access toggled, we need to inform the + // firewall to let the local routes through. The set of routes is passed + // in via stdin encoded in json. + return ft.fwProcEncoder.Encode(allowedRoutes) +} - return nil +func routesEqual(a, b []netaddr.IPPrefix) bool { + if len(a) != len(b) { + return false + } + // Routes are pre-sorted. + for i := range a { + if a[i] != b[i] { + return false + } + } + return true } func strsEqual(a, b []string) bool {