wgengine/router_windows: support toggling local lan access when using

exit nodes.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali 2021-06-16 08:53:08 -07:00 committed by David Crawshaw
parent c37713b927
commit ec52760a3d
3 changed files with 77 additions and 44 deletions

View File

@ -19,6 +19,7 @@
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -27,6 +28,7 @@
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/ipn/ipnserver" "tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy" "tailscale.com/logpolicy"
"tailscale.com/net/dns" "tailscale.com/net/dns"
@ -126,16 +128,6 @@ func beFirewallKillswitch() bool {
log.SetFlags(0) log.SetFlags(0)
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2]) 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]) guid, err := windows.GUIDFromString(os.Args[2])
if err != nil { if err != nil {
log.Fatalf("invalid GUID %q: %v", os.Args[2], err) log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
@ -147,13 +139,25 @@ func beFirewallKillswitch() bool {
} }
start := time.Now() start := time.Now()
if _, err := wf.New(uint64(luid)); err != nil { fw, err := wf.New(uint64(luid))
log.Fatalf("filewall creation failed: %v", err) if err != nil {
log.Fatalf("failed to enable firewall: %v", err)
} }
log.Printf("killswitch enabled, took %s", time.Since(start)) log.Printf("killswitch enabled, took %s", time.Since(start))
// Block until the monitor goroutine shuts us down. // Note(maisem): when local lan access toggled, tailscaled needs to
select {} // 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 { func startIPNServer(ctx context.Context, logid string) error {

View File

@ -2108,7 +2108,7 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
if !default6 { if !default6 {
rs.Routes = append(rs.Routes, ipv6Default) 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. // Only allow local lan access on linux machines for now.
ips, _, err := interfaceRoutes() ips, _, err := interfaceRoutes()
if err != nil { if err != nil {

View File

@ -7,6 +7,7 @@
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -73,7 +74,7 @@ func (r *winRouter) Set(cfg *Config) error {
for _, la := range cfg.LocalAddrs { for _, la := range cfg.LocalAddrs {
localAddrs = append(localAddrs, la.String()) localAddrs = append(localAddrs, la.String())
} }
r.firewall.set(localAddrs, cfg.Routes) r.firewall.set(localAddrs, cfg.Routes, cfg.LocalRoutes)
err := configureInterface(cfg, r.nativeTun) err := configureInterface(cfg, r.nativeTun)
if err != nil { if err != nil {
@ -121,12 +122,16 @@ type firewallTweaker struct {
logf logger.Logf logf logger.Logf
tunGUID windows.GUID tunGUID windows.GUID
mu sync.Mutex mu sync.Mutex
didProcRule bool didProcRule bool
running bool // doAsyncSet goroutine is running running bool // doAsyncSet goroutine is running
known bool // firewall is in known state (in lastVal) known bool // firewall is in known state (in lastVal)
wantLocal []string // next value we want, or "" to delete the firewall rule wantLocal []string // next value we want, or "" to delete the firewall rule
lastLocal []string // last set value, if known lastLocal []string // last set value, if known
localRoutes []netaddr.IPPrefix
lastLocalRoutes []netaddr.IPPrefix
wantKillswitch bool wantKillswitch bool
lastKillswitch bool lastKillswitch bool
@ -138,16 +143,17 @@ type firewallTweaker struct {
// non-nil any number of times during the process's lifetime. // non-nil any number of times during the process's lifetime.
fwProc *exec.Cmd fwProc *exec.Cmd
// stop makes fwProc exit when closed. // 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. // set takes CIDRs to allow, and the routes that point into the Tailscale tun interface.
// Empty slices remove firewall rules. // Empty slices remove firewall rules.
// //
// set takes ownership of cidrs, but not routes. // 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() ft.mu.Lock()
defer ft.mu.Unlock() defer ft.mu.Unlock()
@ -157,6 +163,7 @@ func (ft *firewallTweaker) set(cidrs []string, routes []netaddr.IPPrefix) {
ft.logf("marking allowed %v", cidrs) ft.logf("marking allowed %v", cidrs)
} }
ft.wantLocal = cidrs ft.wantLocal = cidrs
ft.localRoutes = localRoutes
ft.wantKillswitch = hasDefaultRoute(routes) ft.wantKillswitch = hasDefaultRoute(routes)
if ft.running { if ft.running {
// The doAsyncSet goroutine will check ft.wantLocal/wantKillswitch // The doAsyncSet goroutine will check ft.wantLocal/wantKillswitch
@ -184,7 +191,7 @@ func (ft *firewallTweaker) doAsyncSet() {
ft.mu.Lock() ft.mu.Lock()
for { // invariant: ft.mu must be locked when beginning this block for { // invariant: ft.mu must be locked when beginning this block
val := ft.wantLocal 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.running = false
ft.logf("ending netsh goroutine") ft.logf("ending netsh goroutine")
ft.mu.Unlock() ft.mu.Unlock()
@ -193,13 +200,18 @@ func (ft *firewallTweaker) doAsyncSet() {
wantKillswitch := ft.wantKillswitch wantKillswitch := ft.wantKillswitch
needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0 needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0
needProcRule := !ft.didProcRule needProcRule := !ft.didProcRule
localRoutes := ft.localRoutes
ft.mu.Unlock() 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) bo.BackOff(ctx, err)
ft.mu.Lock() ft.mu.Lock()
ft.lastLocal = val ft.lastLocal = val
ft.lastLocalRoutes = localRoutes
ft.lastKillswitch = wantKillswitch ft.lastKillswitch = wantKillswitch
ft.known = (err == nil) ft.known = (err == nil)
} }
@ -218,7 +230,7 @@ func (ft *firewallTweaker) doAsyncSet() {
// process to dial out as it pleases. // process to dial out as it pleases.
// //
// Must only be invoked from doAsyncSet. // 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 { if clear {
ft.logf("clearing Tailscale-In firewall rules...") ft.logf("clearing Tailscale-In firewall rules...")
// We ignore the error here, because netsh returns an error for // 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) 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() exe, err := os.Executable()
if err != nil { if err != nil {
return err return err
} }
proc := exec.Command(exe, "/firewall", ft.tunGUID.String()) proc := exec.Command(exe, "/firewall", ft.tunGUID.String())
var ( in, err := proc.StdinPipe()
out io.ReadCloser
in io.WriteCloser
)
out, err = proc.StdoutPipe()
if err != nil { if err != nil {
return err return err
} }
proc.Stderr = proc.Stdout out, err := proc.StdoutPipe()
in, err = proc.StdinPipe()
if err != nil { if err != nil {
out.Close() in.Close()
return err return err
} }
@ -305,20 +322,32 @@ func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, pr
} }
} }
}(out) }(out)
proc.Stderr = proc.Stdout
if err := proc.Start(); err != nil { if err := proc.Start(); err != nil {
return err return err
} }
ft.stop = in ft.fwProcWriter = in
ft.fwProc = proc ft.fwProc = proc
} else if !killswitch && ft.fwProc != nil { ft.fwProcEncoder = json.NewEncoder(in)
ft.stop.Close()
ft.stop = nil
ft.fwProc.Wait()
ft.fwProc = nil
} }
// 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 { func strsEqual(a, b []string) bool {