mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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:
parent
c37713b927
commit
ec52760a3d
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user