From 34188d93d44f62c2ca8dc2d2d730743707c5c0ae Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 07:55:30 -0800 Subject: [PATCH 01/48] wgengine/monitor: start moving interface state accessor into monitor Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor.go | 11 +++++++++++ wgengine/userspace.go | 16 +++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 5c15f3df3..2d4eb8599 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "tailscale.com/net/interfaces" "tailscale.com/types/logger" ) @@ -72,6 +73,16 @@ func New(logf logger.Logf) (*Mon, error) { }, nil } +// InterfaceState returns the state of the machine's network interfaces, +// without any Tailscale ones. +func (m *Mon) InterfaceState() (*interfaces.State, error) { + s, err := interfaces.GetState() + if s != nil { + s.RemoveTailscaleInterfaces() + } + return s, err +} + // RegisterChangeCallback adds callback to the set of parties to be // notified (in their own goroutine) when the network state changes. // To remove this callback, call unregister (or close the monitor). diff --git a/wgengine/userspace.go b/wgengine/userspace.go index d1d447484..092d1e92c 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -244,8 +244,6 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ pingers: make(map[wgkey.Key]*pinger), } e.localAddrs.Store(map[netaddr.IP]bool{}) - e.linkState, _ = getLinkState() - logf("link state: %+v", e.linkState) if conf.LinkMonitor != nil { e.linkMon = conf.LinkMonitor @@ -258,6 +256,10 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ e.linkMon = mon e.linkMonOwned = true } + + e.linkState, _ = e.linkMon.InterfaceState() + logf("link state: %+v", e.linkState) + unregisterMonWatch := e.linkMon.RegisterChangeCallback(func() { e.LinkChange(false) tshttpproxy.InvalidateCache() @@ -1280,7 +1282,7 @@ func (e *userspaceEngine) setLinkState(st *interfaces.State) (changed bool, cb f } func (e *userspaceEngine) LinkChange(isExpensive bool) { - cur, err := getLinkState() + cur, err := e.linkMon.InterfaceState() if err != nil { e.logf("LinkChange: interfaces.GetState: %v", err) return @@ -1334,14 +1336,6 @@ func (e *userspaceEngine) AddNetworkMapCallback(cb NetworkMapCallback) func() { } } -func getLinkState() (*interfaces.State, error) { - s, err := interfaces.GetState() - if s != nil { - s.RemoveTailscaleInterfaces() - } - return s, err -} - func (e *userspaceEngine) SetNetInfoCallback(cb NetInfoCallback) { e.magicConn.SetNetInfoCallback(cb) } From d74cddcc5642692965956c1d8d79dce6e5ac4e2e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 26 Feb 2021 14:06:00 -0800 Subject: [PATCH 02/48] wgengine/netstack: add Magic DNS + DNS resolution to SOCKS5 dialing Updates #707 Updates #504 Signed-off-by: Brad Fitzpatrick --- wgengine/netstack/netstack.go | 82 +++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 7fb814151..b79a12695 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -15,7 +15,9 @@ import ( "io" "log" "net" + "strconv" "strings" + "sync" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" @@ -34,6 +36,7 @@ import ( "tailscale.com/net/socks5" "tailscale.com/types/logger" "tailscale.com/types/netmap" + "tailscale.com/util/dnsname" "tailscale.com/wgengine" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" @@ -50,6 +53,9 @@ type Impl struct { e wgengine.Engine mc *magicsock.Conn logf logger.Logf + + mu sync.Mutex + dns map[string]netaddr.IP // Magic DNS names (both base + FQDN) => first IP } const nicID = 1 @@ -120,7 +126,33 @@ func (ns *Impl) Start() error { return nil } +func (ns *Impl) updateDNS(nm *netmap.NetworkMap) { + ns.mu.Lock() + defer ns.mu.Unlock() + ns.dns = make(map[string]netaddr.IP) + suffix := nm.MagicDNSSuffix() + + if nm.Name != "" && len(nm.Addresses) > 0 { + ip := nm.Addresses[0].IP + ns.dns[strings.TrimRight(nm.Name, ".")] = ip + if dnsname.HasSuffix(nm.Name, suffix) { + ns.dns[dnsname.TrimSuffix(nm.Name, suffix)] = ip + } + } + for _, p := range nm.Peers { + if p.Name != "" && len(p.Addresses) > 0 { + ip := p.Addresses[0].IP + ns.dns[strings.TrimRight(p.Name, ".")] = ip + if dnsname.HasSuffix(p.Name, suffix) { + ns.dns[dnsname.TrimSuffix(p.Name, suffix)] = ip + } + } + } +} + func (ns *Impl) updateIPs(nm *netmap.NetworkMap) { + ns.updateDNS(nm) + oldIPs := make(map[tcpip.Address]bool) for _, ip := range ns.ipstack.AllAddresses()[nicID] { oldIPs[ip.AddressWithPrefix.Address] = true @@ -166,10 +198,54 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) { } } -func (ns *Impl) dialContextTCP(ctx context.Context, address string) (*gonet.TCPConn, error) { - remoteIPPort, err := netaddr.ParseIPPort(address) +// resolve resolves addr into an IP:port. +func (ns *Impl) resolve(ctx context.Context, addr string) (netaddr.IPPort, error) { + ipp, pippErr := netaddr.ParseIPPort(addr) + if pippErr == nil { + return ipp, nil + } + host, port, err := net.SplitHostPort(addr) if err != nil { - return nil, fmt.Errorf("could not parse IP:port: %w", err) + // addr is malformed. + return netaddr.IPPort{}, err + } + if net.ParseIP(host) != nil { + // The host part of addr was an IP, so the netaddr.ParseIPPort above should've + // passed. Must've been a bad port number. Return the original error. + return netaddr.IPPort{}, pippErr + } + port16, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return netaddr.IPPort{}, fmt.Errorf("invalid port in address %q", addr) + } + + // Host is not an IP, so assume it's a DNS name. + + // Try MagicDNS first, else otherwise a real DNS lookup. + ns.mu.Lock() + ip := ns.dns[host] + ns.mu.Unlock() + if !ip.IsZero() { + return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil + } + + // No Magic DNS name so try real DNS. + var r net.Resolver + ips, err := r.LookupIP(ctx, "ip", host) + if err != nil { + return netaddr.IPPort{}, err + } + if len(ips) == 0 { + return netaddr.IPPort{}, fmt.Errorf("DNS lookup returned no results for %q", host) + } + ip, _ = netaddr.FromStdIP(ips[0]) + return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil +} + +func (ns *Impl) dialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn, error) { + remoteIPPort, err := ns.resolve(ctx, addr) + if err != nil { + return nil, err } remoteAddress := tcpip.FullAddress{ NIC: nicID, From 38dc6fe75879e11c25ad83e7c17dabc35ef3b37d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 10:08:53 -0800 Subject: [PATCH 03/48] cmd/tailscaled, wgengine: remove --fake, replace with netstack And add a --socks5-server flag. And fix a race in SOCKS5 replies where the response header was written concurrently with the copy from the backend. Co-authored with Naman Sood. Updates #707 Updates #504 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware.txt | 2 +- cmd/tailscaled/tailscaled.go | 83 +++++++++++++++++++++++++++++------ ipn/ipnlocal/loglines_test.go | 2 +- ipn/ipnserver/server_test.go | 2 +- net/socks5/socks5.go | 32 +++++++++----- wgengine/netstack/netstack.go | 66 +++++++++++++--------------- wgengine/userspace.go | 54 +++++++---------------- wgengine/userspace_test.go | 2 +- wgengine/watchdog_test.go | 4 +- 9 files changed, 142 insertions(+), 105 deletions(-) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 1e1df8bf4..cda882c4c 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -96,7 +96,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver tailscale.com/net/packet from tailscale.com/wgengine+ tailscale.com/net/portmapper from tailscale.com/net/netcheck+ - tailscale.com/net/socks5 from tailscale.com/wgengine/netstack + tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled tailscale.com/net/stun from tailscale.com/net/netcheck+ tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index c4eb331c8..d96bff392 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -14,6 +14,7 @@ import ( "flag" "fmt" "log" + "net" "net/http" "net/http/pprof" "os" @@ -21,19 +22,23 @@ import ( "runtime" "runtime/debug" "strconv" + "sync" "syscall" "time" "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" + "tailscale.com/net/socks5" "tailscale.com/paths" "tailscale.com/types/flagtype" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/version" "tailscale.com/wgengine" "tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/netstack" "tailscale.com/wgengine/router" + "tailscale.com/wgengine/tstun" ) // globalStateKey is the ipn.StateKey that tailscaled loads on @@ -62,13 +67,13 @@ func defaultTunName() string { var args struct { cleanup bool - fake bool debug string tunname string port uint16 statepath string socketpath string verbose int + socksAddr string // listen address for SOCKS5 server } var ( @@ -94,9 +99,9 @@ func main() { printVersion := false flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose") flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit") - flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface") flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server") - flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name") + flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`) + flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`) flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select") flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file") flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket") @@ -190,23 +195,73 @@ func run() error { go runDebugServer(debugMux, args.debug) } - var e wgengine.Engine - if args.fake { - var impl wgengine.FakeImplFactory - if args.tunname == "userspace-networking" { - impl = netstack.Create + var socksListener net.Listener + if args.socksAddr != "" { + var err error + socksListener, err = net.Listen("tcp", args.socksAddr) + if err != nil { + log.Fatalf("SOCKS5 listener: %v", err) } - e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl) - } else { - e, err = wgengine.NewUserspaceEngine(logf, wgengine.Config{ - TUNName: args.tunname, - ListenPort: args.port, - }) } + + conf := wgengine.Config{ + ListenPort: args.port, + } + if args.tunname == "userspace-networking" { + conf.TUN = tstun.NewFakeTUN() + conf.RouterGen = router.NewFake + } else { + conf.TUNName = args.tunname + } + + e, err := wgengine.NewUserspaceEngine(logf, conf) if err != nil { logf("wgengine.New: %v", err) return err } + + var ns *netstack.Impl + if args.tunname == "userspace-networking" { + tunDev, magicConn := e.(wgengine.InternalsGetter).GetInternals() + ns, err = netstack.Create(logf, tunDev, e, magicConn) + if err != nil { + log.Fatalf("netstack.Create: %v", err) + } + if err := ns.Start(); err != nil { + log.Fatalf("failed to start netstack: %v", err) + } + } + + if socksListener != nil { + srv := &socks5.Server{ + Logf: logger.WithPrefix(logf, "socks5: "), + } + if args.tunname == "userspace-networking" { + srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { + return ns.DialContextTCP(ctx, addr) + } + } else { + var mu sync.Mutex + var dns netstack.DNSMap + e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) { + mu.Lock() + defer mu.Unlock() + dns = netstack.DNSMapFromNetworkMap(nm) + }) + srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { + ipp, err := dns.Resolve(ctx, addr) + if err != nil { + return nil, err + } + var d net.Dialer + return d.DialContext(ctx, network, ipp.String()) + } + } + go func() { + log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener)) + }() + } + e = wgengine.NewWatchdog(e) ctx, cancel := context.WithCancel(context.Background()) diff --git a/ipn/ipnlocal/loglines_test.go b/ipn/ipnlocal/loglines_test.go index 83ae8a309..812e69037 100644 --- a/ipn/ipnlocal/loglines_test.go +++ b/ipn/ipnlocal/loglines_test.go @@ -41,7 +41,7 @@ func TestLocalLogLines(t *testing.T) { // set up a LocalBackend, super bare bones. No functional data. store := &ipn.MemoryStore{} - e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil) + e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0) if err != nil { t.Fatal(err) } diff --git a/ipn/ipnserver/server_test.go b/ipn/ipnserver/server_test.go index 0d89c1a3a..9a368c17f 100644 --- a/ipn/ipnserver/server_test.go +++ b/ipn/ipnserver/server_test.go @@ -56,7 +56,7 @@ func TestRunMultipleAccepts(t *testing.T) { } } - eng, err := wgengine.NewFakeUserspaceEngine(logf, 0, nil) + eng, err := wgengine.NewFakeUserspaceEngine(logf, 0) if err != nil { t.Fatal(err) } diff --git a/net/socks5/socks5.go b/net/socks5/socks5.go index e59c0ee78..89b7c126b 100644 --- a/net/socks5/socks5.go +++ b/net/socks5/socks5.go @@ -108,7 +108,7 @@ func (s *Server) Serve(l net.Listener) error { conn := &Conn{clientConn: c, srv: s} err := conn.Run() if err != nil { - s.logf("socks5: client connection failed: %v", err) + s.logf("client connection failed: %v", err) conn.clientConn.Close() } }() @@ -123,7 +123,6 @@ type Conn struct { srv *Server clientConn net.Conn - serverConn net.Conn request *request } @@ -153,11 +152,7 @@ func (c *Conn) handleRequest() error { return fmt.Errorf("unsupported command %v", req.command) } c.request = req - return c.createReply() -} -func (c *Conn) createReply() error { - var err error ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() srv, err := c.srv.dial( @@ -171,14 +166,12 @@ func (c *Conn) createReply() error { c.clientConn.Write(buf) return err } - c.serverConn = srv - serverAddr, serverPortStr, err := net.SplitHostPort(c.serverConn.LocalAddr().String()) + defer srv.Close() + serverAddr, serverPortStr, err := net.SplitHostPort(srv.LocalAddr().String()) if err != nil { return err } serverPort, _ := strconv.Atoi(serverPortStr) - go io.Copy(c.clientConn, c.serverConn) - go io.Copy(c.serverConn, c.clientConn) var bindAddrType addrType if ip := net.ParseIP(serverAddr); ip != nil { @@ -190,7 +183,6 @@ func (c *Conn) createReply() error { } else { bindAddrType = domainName } - res := &response{ reply: success, bindAddrType: bindAddrType, @@ -203,7 +195,23 @@ func (c *Conn) createReply() error { buf, _ = res.marshal() } c.clientConn.Write(buf) - return err + + errc := make(chan error, 2) + go func() { + _, err := io.Copy(c.clientConn, srv) + if err != nil { + err = fmt.Errorf("from backend to client: %w", err) + } + errc <- err + }() + go func() { + _, err := io.Copy(srv, c.clientConn) + if err != nil { + err = fmt.Errorf("from client to backend: %w", err) + } + errc <- err + }() + return <-errc } // parseClientGreeting parses a request initiation packet diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index b79a12695..5bf93a8b5 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -33,7 +33,6 @@ import ( "gvisor.dev/gvisor/pkg/waiter" "inet.af/netaddr" "tailscale.com/net/packet" - "tailscale.com/net/socks5" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/util/dnsname" @@ -55,13 +54,13 @@ type Impl struct { logf logger.Logf mu sync.Mutex - dns map[string]netaddr.IP // Magic DNS names (both base + FQDN) => first IP + dns DNSMap } const nicID = 1 // Create creates and populates a new Impl. -func Create(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.Conn) (wgengine.FakeImpl, error) { +func Create(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.Conn) (*Impl, error) { if mc == nil { return nil, errors.New("nil magicsock.Conn") } @@ -121,33 +120,40 @@ func (ns *Impl) Start() error { ns.ipstack.SetTransportProtocolHandler(udp.ProtocolNumber, udpFwd.HandlePacket) go ns.injectOutbound() ns.tundev.PostFilterIn = ns.injectInbound - go ns.socks5Server() - return nil } -func (ns *Impl) updateDNS(nm *netmap.NetworkMap) { - ns.mu.Lock() - defer ns.mu.Unlock() - ns.dns = make(map[string]netaddr.IP) +// DNSMap maps MagicDNS names (both base + FQDN) to their first IP. +// It should not be mutated once created. +type DNSMap map[string]netaddr.IP + +func DNSMapFromNetworkMap(nm *netmap.NetworkMap) DNSMap { + ret := make(DNSMap) suffix := nm.MagicDNSSuffix() if nm.Name != "" && len(nm.Addresses) > 0 { ip := nm.Addresses[0].IP - ns.dns[strings.TrimRight(nm.Name, ".")] = ip + ret[strings.TrimRight(nm.Name, ".")] = ip if dnsname.HasSuffix(nm.Name, suffix) { - ns.dns[dnsname.TrimSuffix(nm.Name, suffix)] = ip + ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip } } for _, p := range nm.Peers { if p.Name != "" && len(p.Addresses) > 0 { ip := p.Addresses[0].IP - ns.dns[strings.TrimRight(p.Name, ".")] = ip + ret[strings.TrimRight(p.Name, ".")] = ip if dnsname.HasSuffix(p.Name, suffix) { - ns.dns[dnsname.TrimSuffix(p.Name, suffix)] = ip + ret[dnsname.TrimSuffix(p.Name, suffix)] = ip } } } + return ret +} + +func (ns *Impl) updateDNS(nm *netmap.NetworkMap) { + ns.mu.Lock() + defer ns.mu.Unlock() + ns.dns = DNSMapFromNetworkMap(nm) } func (ns *Impl) updateIPs(nm *netmap.NetworkMap) { @@ -198,8 +204,9 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) { } } -// resolve resolves addr into an IP:port. -func (ns *Impl) resolve(ctx context.Context, addr string) (netaddr.IPPort, error) { +// Resolve resolves addr into an IP:port using first the MagicDNS contents +// of m, else using the system resolver. +func (m DNSMap) Resolve(ctx context.Context, addr string) (netaddr.IPPort, error) { ipp, pippErr := netaddr.ParseIPPort(addr) if pippErr == nil { return ipp, nil @@ -222,9 +229,7 @@ func (ns *Impl) resolve(ctx context.Context, addr string) (netaddr.IPPort, error // Host is not an IP, so assume it's a DNS name. // Try MagicDNS first, else otherwise a real DNS lookup. - ns.mu.Lock() - ip := ns.dns[host] - ns.mu.Unlock() + ip := m[host] if !ip.IsZero() { return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil } @@ -242,8 +247,12 @@ func (ns *Impl) resolve(ctx context.Context, addr string) (netaddr.IPPort, error return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil } -func (ns *Impl) dialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn, error) { - remoteIPPort, err := ns.resolve(ctx, addr) +func (ns *Impl) DialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn, error) { + ns.mu.Lock() + dnsMap := ns.dns + ns.mu.Unlock() + + remoteIPPort, err := dnsMap.Resolve(ctx, addr) if err != nil { return nil, err } @@ -342,7 +351,7 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, address stri } cancel() }() - server, err := ns.dialContextTCP(ctx, address) + server, err := ns.DialContextTCP(ctx, address) if err != nil { ns.logf("netstack: could not connect to server %s: %s", address, err) return @@ -361,21 +370,6 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, address stri ns.logf("[v2] netstack: forwarder connection to %s closed", address) } -func (ns *Impl) socks5Server() { - ln, err := net.Listen("tcp", "localhost:1080") - if err != nil { - ns.logf("could not start SOCKS5 listener: %v", err) - return - } - srv := &socks5.Server{ - Logf: ns.logf, - Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { - return ns.dialContextTCP(ctx, addr) - }, - } - ns.logf("SOCKS5 server exited: %v", srv.Serve(ln)) -} - func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) { ns.logf("[v2] UDP ForwarderRequest: %v", r) var wq waiter.Queue diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 092d1e92c..d21cd5759 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -131,6 +131,15 @@ type userspaceEngine struct { // Lock ordering: magicsock.Conn.mu, wgLock, then mu. } +// InternalsGetter is implemented by Engines that can export their internals. +type InternalsGetter interface { + GetInternals() (*tstun.TUN, *magicsock.Conn) +} + +func (e *userspaceEngine) GetInternals() (*tstun.TUN, *magicsock.Conn) { + return e.tundev, e.magicConn +} + // RouterGen is the signature for a function that creates a // router.Router. type RouterGen func(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (router.Router, error) @@ -157,36 +166,18 @@ type Config struct { // If zero, a port is automatically selected. ListenPort uint16 - // Fake determines whether this engine is running in fake mode, - // which disables such features as DNS configuration and unrestricted ICMP Echo responses. + // Fake determines whether this engine should automatically + // reply to ICMP pings. Fake bool - - // FakeImplFactory, if non-nil, creates a FakeImpl to use as a fake engine - // implementation. Two values are typical: nil, for a basic ping-only fake - // implementation, and netstack.Create, which creates a userspace network - // stack using gvisor's netstack. The desire to keep netstack out of some - // binaries is why the FakeImpl interface exists, so wgengine need not - // depend on gvisor. - FakeImplFactory FakeImplFactory } -// FakeImpl is a fake or alternate version of Engine that can be started. See -// Config.FakeImplFactory for details. -type FakeImpl interface { - Start() error -} - -// FakeImplFactory is the type of a function used to create FakeImpls. -type FakeImplFactory func(logger.Logf, *tstun.TUN, Engine, *magicsock.Conn) (FakeImpl, error) - -func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16, impl FakeImplFactory) (Engine, error) { +func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) { logf("Starting userspace wireguard engine (with fake TUN device)") return NewUserspaceEngine(logf, Config{ - TUN: tstun.NewFakeTUN(), - RouterGen: router.NewFake, - ListenPort: listenPort, - Fake: true, - FakeImplFactory: impl, + TUN: tstun.NewFakeTUN(), + RouterGen: router.NewFake, + ListenPort: listenPort, + Fake: true, }) } @@ -292,18 +283,7 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ // Respond to all pings only in fake mode. if conf.Fake { - if f := conf.FakeImplFactory; f != nil { - impl, err := f(logf, e.tundev, e, e.magicConn) - if err != nil { - return nil, err - } - if err := impl.Start(); err != nil { - return nil, err - } - } else { - // Respond to all pings only in fake mode. - e.tundev.PostFilterIn = echoRespondToAll - } + e.tundev.PostFilterIn = echoRespondToAll } e.tundev.PreFilterOut = e.handleLocalPackets diff --git a/wgengine/userspace_test.go b/wgengine/userspace_test.go index a5cc9d965..e9b83389a 100644 --- a/wgengine/userspace_test.go +++ b/wgengine/userspace_test.go @@ -84,7 +84,7 @@ func TestNoteReceiveActivity(t *testing.T) { } func TestUserspaceEngineReconfig(t *testing.T) { - e, err := NewFakeUserspaceEngine(t.Logf, 0, nil) + e, err := NewFakeUserspaceEngine(t.Logf, 0) if err != nil { t.Fatal(err) } diff --git a/wgengine/watchdog_test.go b/wgengine/watchdog_test.go index 7b3cc659c..7487d4827 100644 --- a/wgengine/watchdog_test.go +++ b/wgengine/watchdog_test.go @@ -17,7 +17,7 @@ func TestWatchdog(t *testing.T) { t.Run("default watchdog does not fire", func(t *testing.T) { t.Parallel() - e, err := NewFakeUserspaceEngine(t.Logf, 0, nil) + e, err := NewFakeUserspaceEngine(t.Logf, 0) if err != nil { t.Fatal(err) } @@ -35,7 +35,7 @@ func TestWatchdog(t *testing.T) { t.Run("watchdog fires on blocked getStatus", func(t *testing.T) { t.Parallel() - e, err := NewFakeUserspaceEngine(t.Logf, 0, nil) + e, err := NewFakeUserspaceEngine(t.Logf, 0) if err != nil { t.Fatal(err) } From a038e8690cc9476b8e96e82f39856c311ed73f1a Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 11:19:20 -0800 Subject: [PATCH 04/48] wgengine/netstack: fix 32-bit build broken from prior commit Signed-off-by: Brad Fitzpatrick --- Makefile | 5 ++++- wgengine/netstack/netstack_32bit.go | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index dee8743ca..944a77aed 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,10 @@ depaware: buildwindows: GOOS=windows GOARCH=amd64 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled -check: staticcheck vet depaware buildwindows +build386: + GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled + +check: staticcheck vet depaware buildwindows build386 staticcheck: go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork) diff --git a/wgengine/netstack/netstack_32bit.go b/wgengine/netstack/netstack_32bit.go index dfb433d3d..7f3343ada 100644 --- a/wgengine/netstack/netstack_32bit.go +++ b/wgengine/netstack/netstack_32bit.go @@ -8,14 +8,30 @@ package netstack import ( + "context" "errors" + "net" + "inet.af/netaddr" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/wgengine" "tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/tstun" ) -func Create(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.Conn) (wgengine.FakeImpl, error) { +type Impl struct{} + +func (*Impl) Start() error { panic("noimpl") } + +func (*Impl) DialContextTCP(ctx context.Context, addr string) (net.Conn, error) { panic("noimpl") } + +type DNSMap map[string]netaddr.IP + +func (m DNSMap) Resolve(ctx context.Context, addr string) (netaddr.IPPort, error) { panic("noimpl") } + +func DNSMapFromNetworkMap(nm *netmap.NetworkMap) DNSMap { return nil } + +func Create(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.Conn) (*Impl, error) { return nil, errors.New("netstack is not supported on 32-bit platforms for now; see https://github.com/google/gvisor/issues/5241") } From e3df29d488f5ce50ee396b1f05a92e9cf1abb006 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 12:56:03 -0800 Subject: [PATCH 05/48] wgengine{,/monitor}: move interface state fetching/comparing to monitor Gets it out of wgengine so the Engine isn't responsible for being a callback registration hub for it. This also removes the Engine.LinkChange method, as it's no longer necessary. The monitor tells us about changes; it doesn't seem to need any help. (Currently it was only used by Swift, but as of 14dc79013754fe we just do the same from Go) Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/debug.go | 19 +++++++------- ipn/ipnlocal/local.go | 30 +++++++++++----------- wgengine/monitor/monitor.go | 47 +++++++++++++++++++++++++--------- wgengine/userspace.go | 50 ++++++++----------------------------- wgengine/watchdog.go | 15 ++++------- wgengine/wgengine.go | 24 +++--------------- 6 files changed, 79 insertions(+), 106 deletions(-) diff --git a/cmd/tailscaled/debug.go b/cmd/tailscaled/debug.go index e13f361bd..c0d3134a8 100644 --- a/cmd/tailscaled/debug.go +++ b/cmd/tailscaled/debug.go @@ -60,12 +60,7 @@ func debugMode(args []string) error { } func runMonitor(ctx context.Context) error { - dump := func() { - st, err := interfaces.GetState() - if err != nil { - log.Printf("error getting state: %v", err) - return - } + dump := func(st *interfaces.State) { j, _ := json.MarshalIndent(st, "", " ") os.Stderr.Write(j) } @@ -73,12 +68,16 @@ func runMonitor(ctx context.Context) error { if err != nil { return err } - mon.RegisterChangeCallback(func() { - log.Printf("Link monitor fired. State:") - dump() + mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) { + if changed { + log.Printf("Link monitor fired; no change") + return + } + log.Printf("Link monitor fired. New state:") + dump(st) }) log.Printf("Starting link change monitor; initial state:") - dump() + dump(mon.InterfaceState()) mon.Start() log.Printf("Started link change monitor; waiting...") select {} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c9a4ebd2a..f82f19273 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -64,19 +64,20 @@ func getControlDebugFlags() []string { // state machine generates events back out to zero or more components. type LocalBackend struct { // Elements that are thread-safe or constant after construction. - ctx context.Context // canceled by Close - ctxCancel context.CancelFunc // cancels ctx - logf logger.Logf // general logging - keyLogf logger.Logf // for printing list of peers on change - statsLogf logger.Logf // for printing peers stats on change - e wgengine.Engine - store ipn.StateStore - backendLogID string - portpoll *portlist.Poller // may be nil - portpollOnce sync.Once // guards starting readPoller - gotPortPollRes chan struct{} // closed upon first readPoller result - serverURL string // tailcontrol URL - newDecompressor func() (controlclient.Decompressor, error) + ctx context.Context // canceled by Close + ctxCancel context.CancelFunc // cancels ctx + logf logger.Logf // general logging + keyLogf logger.Logf // for printing list of peers on change + statsLogf logger.Logf // for printing peers stats on change + e wgengine.Engine + store ipn.StateStore + backendLogID string + unregisterLinkMon func() + portpoll *portlist.Poller // may be nil + portpollOnce sync.Once // guards starting readPoller + gotPortPollRes chan struct{} // closed upon first readPoller result + serverURL string // tailcontrol URL + newDecompressor func() (controlclient.Decompressor, error) filterHash string @@ -138,7 +139,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge portpoll: portpoll, gotPortPollRes: make(chan struct{}), } - e.SetLinkChangeCallback(b.linkChange) + b.unregisterLinkMon = e.GetLinkMonitor().RegisterChangeCallback(b.linkChange) b.statusChanged = sync.NewCond(&b.statusLock) return b, nil @@ -178,6 +179,7 @@ func (b *LocalBackend) Shutdown() { cli := b.c b.mu.Unlock() + b.unregisterLinkMon() if cli != nil { cli.Shutdown() } diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 2d4eb8599..355da7da9 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -33,9 +33,11 @@ type osMon interface { Receive() (message, error) } -// ChangeFunc is a callback function that's called when -// an interface status changes. -type ChangeFunc func() +// ChangeFunc is a callback function that's called when the network +// changed. The changed parameter is whether the network changed +// enough for interfaces.State to have changed since the last +// callback. +type ChangeFunc func(changed bool, state *interfaces.State) // An allocated callbackHandle's address is the Mon.cbs map key. type callbackHandle byte @@ -47,8 +49,9 @@ type Mon struct { change chan struct{} stop chan struct{} - mu sync.Mutex // guards cbs - cbs map[*callbackHandle]ChangeFunc + mu sync.Mutex // guards cbs + cbs map[*callbackHandle]ChangeFunc + ifState *interfaces.State onceStart sync.Once started bool @@ -64,18 +67,30 @@ func New(logf logger.Logf) (*Mon, error) { if err != nil { return nil, err } - return &Mon{ + m := &Mon{ logf: logf, cbs: map[*callbackHandle]ChangeFunc{}, om: om, change: make(chan struct{}, 1), stop: make(chan struct{}), - }, nil + } + st, err := m.interfaceStateUncached() + if err != nil { + return nil, err + } + m.ifState = st + return m, nil } // InterfaceState returns the state of the machine's network interfaces, // without any Tailscale ones. -func (m *Mon) InterfaceState() (*interfaces.State, error) { +func (m *Mon) InterfaceState() *interfaces.State { + m.mu.Lock() + defer m.mu.Unlock() + return m.ifState +} + +func (m *Mon) interfaceStateUncached() (*interfaces.State, error) { s, err := interfaces.GetState() if s != nil { s.RemoveTailscaleInterfaces() @@ -167,11 +182,19 @@ func (m *Mon) debounce() { case <-m.change: } - m.mu.Lock() - for _, cb := range m.cbs { - go cb() + if curState, err := m.interfaceStateUncached(); err != nil { + m.logf("interfaces.State: %v", err) + } else { + m.mu.Lock() + changed := !curState.Equal(m.ifState) + if changed { + m.ifState = curState + } + for _, cb := range m.cbs { + go cb(changed, m.ifState) + } + m.mu.Unlock() } - m.mu.Unlock() select { case <-m.stop: diff --git a/wgengine/userspace.go b/wgengine/userspace.go index d21cd5759..076eefa70 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -120,11 +120,9 @@ type userspaceEngine struct { mu sync.Mutex // guards following; see lock order comment below closing bool // Close was called (even if we're still closing) statusCallback StatusCallback - linkChangeCallback func(major bool, newState *interfaces.State) peerSequence []wgkey.Key endpoints []string - pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers - linkState *interfaces.State + pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go networkMapCallbacks map[*someHandle]NetworkMapCallback @@ -248,12 +246,11 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ e.linkMonOwned = true } - e.linkState, _ = e.linkMon.InterfaceState() - logf("link state: %+v", e.linkState) + logf("link state: %+v", e.linkMon.InterfaceState()) - unregisterMonWatch := e.linkMon.RegisterChangeCallback(func() { - e.LinkChange(false) + unregisterMonWatch := e.linkMon.RegisterChangeCallback(func(changed bool, st *interfaces.State) { tshttpproxy.InvalidateCache() + e.linkChange(false, st) }) closePool.addFunc(unregisterMonWatch) e.linkMonUnregister = unregisterMonWatch @@ -279,7 +276,7 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ return nil, fmt.Errorf("wgengine: %v", err) } closePool.add(e.magicConn) - e.magicConn.SetNetworkUp(e.linkState.AnyInterfaceUp()) + e.magicConn.SetNetworkUp(e.linkMon.InterfaceState().AnyInterfaceUp()) // Respond to all pings only in fake mode. if conf.Fake { @@ -1250,30 +1247,15 @@ func (e *userspaceEngine) Wait() { <-e.waitCh } -func (e *userspaceEngine) setLinkState(st *interfaces.State) (changed bool, cb func(major bool, newState *interfaces.State)) { - if st == nil { - return false, nil - } - e.mu.Lock() - defer e.mu.Unlock() - changed = e.linkState == nil || !st.Equal(e.linkState) - e.linkState = st - return changed, e.linkChangeCallback +func (e *userspaceEngine) GetLinkMonitor() *monitor.Mon { + return e.linkMon } -func (e *userspaceEngine) LinkChange(isExpensive bool) { - cur, err := e.linkMon.InterfaceState() - if err != nil { - e.logf("LinkChange: interfaces.GetState: %v", err) - return - } - cur.IsExpensive = isExpensive - needRebind, linkChangeCallback := e.setLinkState(cur) - +func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) { up := cur.AnyInterfaceUp() if !up { e.logf("LinkChange: all links down; pausing: %v", cur) - } else if needRebind { + } else if changed { e.logf("LinkChange: major, rebinding. New state: %v", cur) } else { e.logf("[v1] LinkChange: minor") @@ -1282,23 +1264,11 @@ func (e *userspaceEngine) LinkChange(isExpensive bool) { e.magicConn.SetNetworkUp(up) why := "link-change-minor" - if needRebind { + if changed { why = "link-change-major" e.magicConn.Rebind() } e.magicConn.ReSTUN(why) - if linkChangeCallback != nil { - go linkChangeCallback(needRebind, cur) - } -} - -func (e *userspaceEngine) SetLinkChangeCallback(cb func(major bool, newState *interfaces.State)) { - e.mu.Lock() - defer e.mu.Unlock() - e.linkChangeCallback = cb - if e.linkState != nil { - go cb(false, e.linkState) - } } func (e *userspaceEngine) AddNetworkMapCallback(cb NetworkMapCallback) func() { diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index 130ce4610..bcef4e160 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -14,10 +14,10 @@ import ( "inet.af/netaddr" "tailscale.com/ipn/ipnstate" - "tailscale.com/net/interfaces" "tailscale.com/tailcfg" "tailscale.com/types/netmap" "tailscale.com/wgengine/filter" + "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" @@ -75,10 +75,11 @@ func (e *watchdogEngine) watchdog(name string, fn func()) { func (e *watchdogEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config) error { return e.watchdogErr("Reconfig", func() error { return e.wrap.Reconfig(cfg, routerCfg) }) } +func (e *watchdogEngine) GetLinkMonitor() *monitor.Mon { + return e.wrap.GetLinkMonitor() +} func (e *watchdogEngine) GetFilter() *filter.Filter { - var x *filter.Filter - e.watchdog("GetFilter", func() { x = e.wrap.GetFilter() }) - return x + return e.wrap.GetFilter() } func (e *watchdogEngine) SetFilter(filt *filter.Filter) { e.watchdog("SetFilter", func() { e.wrap.SetFilter(filt) }) @@ -98,12 +99,6 @@ func (e *watchdogEngine) SetNetInfoCallback(cb NetInfoCallback) { func (e *watchdogEngine) RequestStatus() { e.watchdog("RequestStatus", func() { e.wrap.RequestStatus() }) } -func (e *watchdogEngine) LinkChange(isExpensive bool) { - e.watchdog("LinkChange", func() { e.wrap.LinkChange(isExpensive) }) -} -func (e *watchdogEngine) SetLinkChangeCallback(cb func(major bool, newState *interfaces.State)) { - e.watchdog("SetLinkChangeCallback", func() { e.wrap.SetLinkChangeCallback(cb) }) -} func (e *watchdogEngine) SetDERPMap(m *tailcfg.DERPMap) { e.watchdog("SetDERPMap", func() { e.wrap.SetDERPMap(m) }) } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 257d59f26..a72d0a96a 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -9,10 +9,10 @@ import ( "inet.af/netaddr" "tailscale.com/ipn/ipnstate" - "tailscale.com/net/interfaces" "tailscale.com/tailcfg" "tailscale.com/types/netmap" "tailscale.com/wgengine/filter" + "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" @@ -72,6 +72,9 @@ type Engine interface { // WireGuard status changes. SetStatusCallback(StatusCallback) + // GetLinkMonitor returns the link monitor. + GetLinkMonitor() *monitor.Mon + // RequestStatus requests a WireGuard status update right // away, sent to the callback registered via SetStatusCallback. RequestStatus() @@ -86,18 +89,6 @@ type Engine interface { // TODO: return an error? Wait() - // LinkChange informs the engine that the system network - // link has changed. The isExpensive parameter is set on links - // where sending packets uses substantial power or money, - // such as mobile data on a phone. - // - // LinkChange should be called whenever something changed with - // the network, no matter how minor. The implementation should - // look at the state of the network and decide whether the - // change from before is interesting enough to warrant taking - // action on. - LinkChange(isExpensive bool) - // SetDERPMap controls which (if any) DERP servers are used. // If nil, DERP is disabled. It starts disabled until a DERP map // is configured. @@ -120,13 +111,6 @@ type Engine interface { // new NetInfo summary is available. SetNetInfoCallback(NetInfoCallback) - // SetLinkChangeCallback sets the function to call when the - // link state changes. - // The provided function is run in a new goroutine once upon - // initial call (if the engine has a known link state) and - // upon any change. - SetLinkChangeCallback(func(major bool, newState *interfaces.State)) - // DiscoPublicKey gets the public key used for path discovery // messages. DiscoPublicKey() tailcfg.DiscoKey From 03c344333e26a42720088cf161ba2b274b56f82f Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 15:31:10 -0800 Subject: [PATCH 06/48] cmd/tailscale: remove Windows console fixing Not needed, as we don't build this as a GUI app ever. Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/depaware.txt | 4 +--- cmd/tailscale/tailscale.go | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 20b149957..6bdf761c1 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -2,8 +2,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy - github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale - W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck @@ -80,7 +78,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep golang.org/x/sync/singleflight from tailscale.com/net/dnscache golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+ LD golang.org/x/sys/unix from tailscale.com/net/netns+ - W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+ + W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+ W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg golang.org/x/text/secure/bidirule from golang.org/x/net/idna golang.org/x/text/transform from golang.org/x/text/secure/bidirule+ diff --git a/cmd/tailscale/tailscale.go b/cmd/tailscale/tailscale.go index 2fda32f9b..39d8bf955 100644 --- a/cmd/tailscale/tailscale.go +++ b/cmd/tailscale/tailscale.go @@ -8,19 +8,12 @@ package main // import "tailscale.com/cmd/tailscale" import ( "fmt" - "log" "os" - "github.com/apenwarr/fixconsole" "tailscale.com/cmd/tailscale/cli" ) func main() { - err := fixconsole.FixConsoleIfNeeded() - if err != nil { - log.Printf("fixConsoleOutput: %v\n", err) - } - if err := cli.Run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) From 9df4185c94f95d21c62dacc98c12ec517c128b64 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 26 Feb 2021 12:49:54 -0800 Subject: [PATCH 07/48] control/controlclient, net/{dnscache,dnsfallback}: add DNS fallback mechanism Updates #1405 Updates #1403 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware.txt | 3 +- control/controlclient/direct.go | 11 ++-- net/dnscache/dnscache.go | 100 ++++++++++++++++++++++++++++++- net/dnsfallback/dnsfallback.go | 103 ++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 net/dnsfallback/dnsfallback.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index cda882c4c..425cb1641 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -71,7 +71,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ tailscale.com/derp from tailscale.com/derp/derphttp+ tailscale.com/derp/derphttp from tailscale.com/net/netcheck+ - tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled + tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+ tailscale.com/disco from tailscale.com/derp+ tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+ @@ -89,6 +89,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/logtail/filch from tailscale.com/logpolicy tailscale.com/metrics from tailscale.com/derp tailscale.com/net/dnscache from tailscale.com/control/controlclient+ + tailscale.com/net/dnsfallback from tailscale.com/control/controlclient tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+ 💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+ tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index be04e143c..6aab70760 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -36,6 +36,7 @@ import ( "tailscale.com/health" "tailscale.com/log/logheap" "tailscale.com/net/dnscache" + "tailscale.com/net/dnsfallback" "tailscale.com/net/netns" "tailscale.com/net/tlsdial" "tailscale.com/net/tshttpproxy" @@ -126,16 +127,18 @@ func NewDirect(opts Options) (*Direct, error) { httpc := opts.HTTPTestClient if httpc == nil { dnsCache := &dnscache.Resolver{ - Forward: dnscache.Get().Forward, // use default cache's forwarder - UseLastGood: true, + Forward: dnscache.Get().Forward, // use default cache's forwarder + UseLastGood: true, + LookupIPFallback: dnsfallback.Lookup, } dialer := netns.NewDialer() tr := http.DefaultTransport.(*http.Transport).Clone() tr.Proxy = tshttpproxy.ProxyFromEnvironment tshttpproxy.SetTransportGetProxyConnectHeader(tr) - tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache) - tr.ForceAttemptHTTP2 = true tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig) + tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache) + tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig) + tr.ForceAttemptHTTP2 = true httpc = &http.Client{Transport: tr} } diff --git a/net/dnscache/dnscache.go b/net/dnscache/dnscache.go index a1a52107d..953a64c51 100644 --- a/net/dnscache/dnscache.go +++ b/net/dnscache/dnscache.go @@ -8,6 +8,8 @@ package dnscache import ( "context" + "crypto/tls" + "errors" "fmt" "log" "net" @@ -18,6 +20,7 @@ import ( "time" "golang.org/x/sync/singleflight" + "inet.af/netaddr" ) var single = &Resolver{ @@ -55,6 +58,10 @@ type Resolver struct { // If nil, net.DefaultResolver is used. Forward *net.Resolver + // LookupIPFallback optionally provides a backup DNS mechanism + // to use if Forward returns an error or no results. + LookupIPFallback func(ctx context.Context, host string) ([]netaddr.IP, error) + // TTL is how long to keep entries cached // // If zero, a default (currently 10 minutes) is used. @@ -198,6 +205,18 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) { ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host)) defer cancel() ips, err := r.fwd().LookupIPAddr(ctx, host) + if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + var fips []netaddr.IP + fips, err = r.LookupIPFallback(ctx, host) + if err == nil { + ips = nil + for _, fip := range fips { + ips = append(ips, *fip.IPAddr()) + } + } + } if err != nil { return nil, nil, err } @@ -269,13 +288,33 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con // Dialer returns a wrapped DialContext func that uses the provided dnsCache. func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc { - return func(ctx context.Context, network, address string) (net.Conn, error) { + return func(ctx context.Context, network, address string) (retConn net.Conn, ret error) { host, port, err := net.SplitHostPort(address) if err != nil { // Bogus. But just let the real dialer return an error rather than // inventing a similar one. return fwd(ctx, network, address) } + defer func() { + // On any failure, assume our DNS is wrong and try our fallback, if any. + if ret == nil || dnsCache.LookupIPFallback == nil { + return + } + ips, err := dnsCache.LookupIPFallback(ctx, host) + if err != nil { + // Return with original error + return + } + for _, ip := range ips { + dst := net.JoinHostPort(ip.String(), port) + if c, err := fwd(ctx, network, dst); err == nil { + retConn = c + ret = nil + return + } + } + }() + ip, ip6, err := dnsCache.LookupIP(ctx, host) if err != nil { return nil, fmt.Errorf("failed to resolve %q: %w", host, err) @@ -300,3 +339,62 @@ func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc { return fwd(ctx, network, dst) } } + +var errTLSHandshakeTimeout = errors.New("timeout doing TLS handshake") + +// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext. +// It returns a *tls.Conn type on success. +// On TLS cert validation failure, it can invoke a backup DNS resolution strategy. +func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Config) DialContextFunc { + tcpDialer := Dialer(fwd, dnsCache) + return func(ctx context.Context, network, address string) (net.Conn, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + tcpConn, err := tcpDialer(ctx, network, address) + if err != nil { + return nil, err + } + + cfg := cloneTLSConfig(tlsConfigBase) + if cfg.ServerName == "" { + cfg.ServerName = host + } + tlsConn := tls.Client(tcpConn, cfg) + + errc := make(chan error, 2) + handshakeCtx, handshakeTimeoutCancel := context.WithTimeout(ctx, 5*time.Second) + defer handshakeTimeoutCancel() + done := make(chan bool) + defer close(done) + go func() { + select { + case <-done: + case <-handshakeCtx.Done(): + errc <- errTLSHandshakeTimeout + } + }() + go func() { + err := tlsConn.Handshake() + handshakeTimeoutCancel() + errc <- err + }() + if err := <-errc; err != nil { + tcpConn.Close() + // TODO: if err != errTLSHandshakeTimeout, + // assume it might be some captive portal or + // otherwise incorrect DNS and try the backup + // DNS mechanism. + return nil, err + } + return tlsConn, nil + } +} + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return cfg.Clone() +} diff --git a/net/dnsfallback/dnsfallback.go b/net/dnsfallback/dnsfallback.go new file mode 100644 index 000000000..9039dee1e --- /dev/null +++ b/net/dnsfallback/dnsfallback.go @@ -0,0 +1,103 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package dnsfallback contains a DNS fallback mechanism +// for starting up Tailscale when the system DNS is broken or otherwise unavailable. +package dnsfallback + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "math/rand" + "net" + "net/http" + "net/url" + "time" + + "inet.af/netaddr" + "tailscale.com/derp/derpmap" + "tailscale.com/net/netns" + "tailscale.com/net/tshttpproxy" +) + +func Lookup(ctx context.Context, host string) ([]netaddr.IP, error) { + type nameIP struct { + dnsName string + ip netaddr.IP + } + + var cands []nameIP + dm := derpmap.Prod() + for _, dr := range dm.Regions { + for _, n := range dr.Nodes { + if ip, err := netaddr.ParseIP(n.IPv4); err == nil { + cands = append(cands, nameIP{n.HostName, ip}) + } + if ip, err := netaddr.ParseIP(n.IPv6); err == nil { + cands = append(cands, nameIP{n.HostName, ip}) + } + } + } + rand.Shuffle(len(cands), func(i, j int) { + cands[i], cands[j] = cands[j], cands[i] + }) + if len(cands) == 0 { + return nil, fmt.Errorf("no DNS fallback options for %q", host) + } + for ctx.Err() == nil && len(cands) > 0 { + cand := cands[0] + log.Printf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host) + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host) + if err != nil { + log.Printf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err) + continue + } + if ips := dm[host]; len(ips) > 0 { + log.Printf("bootstrapDNS(%q, %q) for %q = %v", cand.dnsName, cand.ip, host, ips) + return ips, nil + } + } + if err := ctx.Err(); err != nil { + return nil, err + } + return nil, fmt.Errorf("no DNS fallback candidates remain for %q", host) +} + +// serverName and serverIP of are, say, "derpN.tailscale.com". +// queryName is the name being sought (e.g. "login.tailscale.com"), passed as hint. +func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netaddr.IP, queryName string) (dnsMap, error) { + dialer := netns.NewDialer() + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.Proxy = tshttpproxy.ProxyFromEnvironment + tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443")) + } + c := &http.Client{Transport: tr} + req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil) + if err != nil { + return nil, err + } + dm := make(dnsMap) + res, err := c.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return nil, errors.New(res.Status) + } + if err := json.NewDecoder(res.Body).Decode(&dm); err != nil { + return nil, err + } + return dm, nil +} + +// dnsMap is the JSON type returned by the DERP /bootstrap-dns handler: +// https://derp10.tailscale.com/bootstrap-dns +type dnsMap map[string][]netaddr.IP From b46e337cdcc50b261bb33356097718e47f6ba735 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 18:47:27 -0800 Subject: [PATCH 08/48] cmd/hello: use go:embed for the template Signed-off-by: Brad Fitzpatrick --- cmd/hello/hello.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go index c31ccb703..61e6d15ac 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -7,6 +7,7 @@ package main // import "tailscale.com/cmd/hello" import ( "context" + _ "embed" "encoding/json" "flag" "fmt" @@ -30,6 +31,9 @@ var ( testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server") ) +//go:embed hello.tmpl.html +var embeddedTemplate string + func main() { flag.Parse() if *testIP != "" { @@ -43,7 +47,10 @@ func main() { return } if !devMode() { - tmpl = template.Must(template.New("home").Parse(slurpHTML())) + if embeddedTemplate == "" { + log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+") + } + tmpl = template.Must(template.New("home").Parse(embeddedTemplate)) } http.HandleFunc("/", root) From f647e3daaf0c8a944fd94daaba531a7f9ab0ee22 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 22 Feb 2021 20:43:35 -0800 Subject: [PATCH 09/48] ipn/ipnlocal: transform default routes into "all but LAN" routes. Fixes #1177. Signed-off-by: David Anderson --- ipn/ipnlocal/local.go | 59 +++++++++++++++++++++++++-- ipn/ipnlocal/local_test.go | 53 ++++++++++++++++++++++++ net/interfaces/interfaces.go | 78 ++++++++++++++++++++---------------- 3 files changed, 153 insertions(+), 37 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index f82f19273..3fd31375b 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -170,6 +170,10 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) { go b.authReconfig() } } + + // If the local network configuration has changed, our filter may + // need updating to tweak default routes. + b.updateFilter(b.netMap, b.prefs) } // Shutdown halts the backend and all its sub-components. The backend @@ -606,9 +610,22 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) } if prefs != nil { for _, r := range prefs.AdvertiseRoutes { - // TODO: when advertising default routes, trim out local - // nets. - localNetsB.AddPrefix(r) + if r.Bits == 0 { + // When offering a default route to the world, we + // filter out locally reachable LANs, so that the + // default route effectively appears to be a "guest + // wifi": you get internet access, but to additionally + // get LAN access the LAN(s) need to be offered + // explicitly as well. + s, err := shrinkDefaultRoute(r) + if err != nil { + b.logf("computing default route filter: %v", err) + continue + } + localNetsB.AddSet(s) + } else { + localNetsB.AddPrefix(r) + } } } localNets := localNetsB.IPSet() @@ -634,6 +651,42 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) } } +var removeFromDefaultRoute = []netaddr.IPPrefix{ + // RFC1918 LAN ranges + netaddr.MustParseIPPrefix("192.168.0.0/16"), + netaddr.MustParseIPPrefix("172.16.0.0/12"), + netaddr.MustParseIPPrefix("10.0.0.0/8"), + // Tailscale IPv4 range + tsaddr.CGNATRange(), + // IPv6 Link-local addresses + netaddr.MustParseIPPrefix("fe80::/10"), + // Tailscale IPv6 range + tsaddr.TailscaleULARange(), +} + +// shrinkDefaultRoute returns an IPSet representing the IPs in route, +// minus those in removeFromDefaultRoute and local interface subnets. +func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) { + var b netaddr.IPSetBuilder + b.AddPrefix(route) + err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) { + if tsaddr.IsTailscaleIP(pfx.IP) { + return + } + if pfx.IsSingleIP() { + return + } + b.RemovePrefix(pfx) + }) + if err != nil { + return nil, err + } + for _, pfx := range removeFromDefaultRoute { + b.RemovePrefix(pfx) + } + return b.IPSet(), nil +} + // dnsCIDRsEqual determines whether two CIDR lists are equal // for DNS map construction purposes (that is, only the first entry counts). func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool { diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 9f19a08f1..f8c1d88df 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -8,6 +8,7 @@ import ( "testing" "inet.af/netaddr" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/netmap" ) @@ -118,3 +119,55 @@ func TestNetworkMapCompare(t *testing.T) { } } } + +func TestShrinkDefaultRoute(t *testing.T) { + tests := []struct { + route string + in []string + out []string + }{ + { + route: "0.0.0.0/0", + in: []string{"1.2.3.4", "25.0.0.1"}, + out: []string{ + "10.0.0.1", + "10.255.255.255", + "192.168.0.1", + "192.168.255.255", + "172.16.0.1", + "172.31.255.255", + "100.101.102.103", + // Some random IPv6 stuff that shouldn't be in a v4 + // default route. + "fe80::", + "2601::1", + }, + }, + { + route: "::/0", + in: []string{"::1", "2601::1"}, + out: []string{ + "fe80::1", + tsaddr.TailscaleULARange().IP.String(), + }, + }, + } + + for _, test := range tests { + def := netaddr.MustParseIPPrefix(test.route) + got, err := shrinkDefaultRoute(def) + if err != nil { + t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err) + } + for _, ip := range test.in { + if !got.Contains(netaddr.MustParseIP(ip)) { + t.Errorf("shrink(%q).Contains(%v) = false, want true", test.route, ip) + } + } + for _, ip := range test.out { + if got.Contains(netaddr.MustParseIP(ip)) { + t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip) + } + } + } +} diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index a0843ab99..02afd44ba 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -135,8 +135,10 @@ type Interface struct { func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) } func (i Interface) IsUp() bool { return isUp(i.Interface) } -// ForeachInterfaceAddress calls fn for each interface's address on the machine. -func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error { +// ForeachInterfaceAddress calls fn for each interface's address on +// the machine. The IPPrefix's IP is the IP address assigned to the +// interface, and Bits are the subnet mask. +func ForeachInterfaceAddress(fn func(Interface, netaddr.IPPrefix)) error { ifaces, err := net.Interfaces() if err != nil { return err @@ -150,8 +152,8 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error { for _, a := range addrs { switch v := a.(type) { case *net.IPNet: - if ip, ok := netaddr.FromStdIP(v.IP); ok { - fn(Interface{iface}, ip) + if pfx, ok := netaddr.FromStdIPNet(v); ok { + fn(Interface{iface}, pfx) } } } @@ -159,8 +161,10 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error { return nil } -// ForeachInterface calls fn for each interface on the machine, with all its addresses. -func ForeachInterface(fn func(Interface, []netaddr.IP)) error { +// ForeachInterface calls fn for each interface on the machine, with +// all its addresses. The IPPrefix's IP is the IP address assigned to +// the interface, and Bits are the subnet mask. +func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error { ifaces, err := net.Interfaces() if err != nil { return err @@ -171,16 +175,16 @@ func ForeachInterface(fn func(Interface, []netaddr.IP)) error { if err != nil { return err } - var ips []netaddr.IP + var pfxs []netaddr.IPPrefix for _, a := range addrs { switch v := a.(type) { case *net.IPNet: - if ip, ok := netaddr.FromStdIP(v.IP); ok { - ips = append(ips, ip) + if pfx, ok := netaddr.FromStdIPNet(v); ok { + pfxs = append(pfxs, pfx) } } } - fn(Interface{iface}, ips) + fn(Interface{iface}, pfxs) } return nil } @@ -189,7 +193,11 @@ func ForeachInterface(fn func(Interface, []netaddr.IP)) error { // routing table, and other network configuration. // For now it's pretty basic. type State struct { - InterfaceIPs map[string][]netaddr.IP + // InterfaceIPs maps from an interface name to the IP addresses + // configured on that interface. Each address is represented as an + // IPPrefix, where the IP is the interface IP address and Bits is + // the subnet mask. + InterfaceIPs map[string][]netaddr.IPPrefix InterfaceUp map[string]bool // HaveV6Global is whether this machine has an IPv6 global address @@ -242,14 +250,14 @@ func (s *State) String() string { if s.InterfaceUp[ifName] { fmt.Fprintf(&sb, "%s:[", ifName) needSpace := false - for _, ip := range s.InterfaceIPs[ifName] { - if !isInterestingIP(ip) { + for _, pfx := range s.InterfaceIPs[ifName] { + if !isInterestingIP(pfx.IP) { continue } if needSpace { sb.WriteString(" ") } - fmt.Fprintf(&sb, "%s", ip) + fmt.Fprintf(&sb, "%s", pfx) needSpace = true } sb.WriteString("]") @@ -287,24 +295,24 @@ func (s *State) AnyInterfaceUp() bool { // are owned by this process. (TODO: make this true; currently it // uses some heuristics) func (s *State) RemoveTailscaleInterfaces() { - for name, ips := range s.InterfaceIPs { - if isTailscaleInterface(name, ips) { + for name, pfxs := range s.InterfaceIPs { + if isTailscaleInterface(name, pfxs) { delete(s.InterfaceIPs, name) delete(s.InterfaceUp, name) } } } -func hasTailscaleIP(ips []netaddr.IP) bool { - for _, ip := range ips { - if tsaddr.IsTailscaleIP(ip) { +func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool { + for _, pfx := range pfxs { + if tsaddr.IsTailscaleIP(pfx.IP) { return true } } return false } -func isTailscaleInterface(name string, ips []netaddr.IP) bool { +func isTailscaleInterface(name string, ips []netaddr.IPPrefix) bool { if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) { // On macOS in the sandboxed app (at least as of // 2021-02-25), we often see two utun devices @@ -326,22 +334,22 @@ var getPAC func() string // It does not set the returned State.IsExpensive. The caller can populate that. func GetState() (*State, error) { s := &State{ - InterfaceIPs: make(map[string][]netaddr.IP), + InterfaceIPs: make(map[string][]netaddr.IPPrefix), InterfaceUp: make(map[string]bool), } - if err := ForeachInterface(func(ni Interface, ips []netaddr.IP) { + if err := ForeachInterface(func(ni Interface, pfxs []netaddr.IPPrefix) { ifUp := ni.IsUp() s.InterfaceUp[ni.Name] = ifUp - s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ips...) - if !ifUp || isTailscaleInterface(ni.Name, ips) { + s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...) + if !ifUp || isTailscaleInterface(ni.Name, pfxs) { return } - for _, ip := range ips { - if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + for _, pfx := range pfxs { + if pfx.IP.IsLoopback() || pfx.IP.IsLinkLocalUnicast() { continue } - s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip) - s.HaveV4 = s.HaveV4 || ip.Is4() + s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP) + s.HaveV4 = s.HaveV4 || pfx.IP.Is4() } }); err != nil { return nil, err @@ -375,7 +383,8 @@ func HTTPOfListener(ln net.Listener) string { var goodIP string var privateIP string - ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) { + ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) { + ip := pfx.IP if isPrivateIP(ip) { if privateIP == "" { privateIP = ip.String() @@ -411,7 +420,8 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) { if !ok { return } - ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) { + ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) { + ip := pfx.IP if !i.IsUp() || ip.IsZero() || !myIP.IsZero() { return } @@ -451,11 +461,11 @@ var ( v6Global1 = mustCIDR("2000::/3") ) -// anyInterestingIP reports ips contains any IP that matches +// anyInterestingIP reports whether pfxs contains any IP that matches // isInterestingIP. -func anyInterestingIP(ips []netaddr.IP) bool { - for _, ip := range ips { - if isInterestingIP(ip) { +func anyInterestingIP(pfxs []netaddr.IPPrefix) bool { + for _, pfx := range pfxs { + if isInterestingIP(pfx.IP) { return true } } From 30a37622b4875ae3d9701dede949bb5c5912a41c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 19:00:37 -0800 Subject: [PATCH 10/48] cmd/hello: break out local HTTP client into client/tailscale Signed-off-by: Brad Fitzpatrick --- client/tailscale/tailscale.go | 90 ++++++++++++++++++++++++++++++++++ cmd/hello/hello.go | 92 +++++++++-------------------------- 2 files changed, 113 insertions(+), 69 deletions(-) create mode 100644 client/tailscale/tailscale.go diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go new file mode 100644 index 000000000..69fc84eb1 --- /dev/null +++ b/client/tailscale/tailscale.go @@ -0,0 +1,90 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package tailscale contains Tailscale client code. +package tailscale + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strconv" + + "tailscale.com/safesocket" + "tailscale.com/tailcfg" +) + +// tsClient does HTTP requests to the local Tailscale daemon. +var tsClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if addr != "local-tailscaled.sock:80" { + return nil, fmt.Errorf("unexpected URL address %q", addr) + } + // On macOS, when dialing from non-sandboxed program to sandboxed GUI running + // a TCP server on a random port, find the random port. For HTTP connections, + // we don't send the token. It gets added in an HTTP Basic-Auth header. + if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil { + var d net.Dialer + return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port)) + } + return safesocket.ConnectDefault() + }, + }, +} + +// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon. +// +// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4. +// +// The hostname must be "local-tailscaled.sock", even though it +// doesn't actually do any DNS lookup. The actual means of connecting to and +// authenticating to the local Tailscale daemon vary by platform. +// +// DoLocalRequest may mutate the request to add Authorization headers. +func DoLocalRequest(req *http.Request) (*http.Response, error) { + if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { + req.SetBasicAuth("", token) + } + return tsClient.Do(req) +} + +// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port. +func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) { + var ip string + if net.ParseIP(remoteAddr) != nil { + ip = remoteAddr + } else { + var err error + ip, _, err = net.SplitHostPort(remoteAddr) + if err != nil { + return nil, fmt.Errorf("invalid remoteAddr %q", remoteAddr) + } + } + req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil) + if err != nil { + return nil, err + } + res, err := DoLocalRequest(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + slurp, _ := ioutil.ReadAll(res.Body) + if res.StatusCode != 200 { + return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp) + } + r := new(tailcfg.WhoIsResponse) + if err := json.Unmarshal(slurp, r); err != nil { + if max := 200; len(slurp) > max { + slurp = slurp[:max] + } + return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp) + } + return r, nil +} diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go index 61e6d15ac..fc7343150 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -10,19 +10,15 @@ import ( _ "embed" "encoding/json" "flag" - "fmt" "html/template" "io/ioutil" "log" "net" "net/http" - "net/url" "os" - "strconv" "strings" - "tailscale.com/safesocket" - "tailscale.com/tailcfg" + "tailscale.com/client/tailscale" ) var ( @@ -37,7 +33,7 @@ var embeddedTemplate string func main() { flag.Parse() if *testIP != "" { - res, err := whoIs(*testIP) + res, err := tailscale.WhoIs(context.Background(), *testIP) if err != nil { log.Fatal(err) } @@ -46,7 +42,14 @@ func main() { e.Encode(res) return } - if !devMode() { + if devMode() { + // Parse it optimistically + var err error + tmpl, err = template.New("home").Parse(embeddedTemplate) + if err != nil { + log.Printf("ignoring template error in dev mode: %v", err) + } + } else { if embeddedTemplate == "" { log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+") } @@ -76,24 +79,24 @@ func main() { log.Fatal(<-errc) } -func slurpHTML() string { - slurp, err := ioutil.ReadFile("hello.tmpl.html") - if err != nil { - log.Fatal(err) - } - return string(slurp) -} - func devMode() bool { return *httpsAddr == "" && *httpAddr != "" } func getTmpl() (*template.Template, error) { if devMode() { - return template.New("home").Parse(slurpHTML()) + tmplData, err := ioutil.ReadFile("hello.tmpl.html") + if os.IsNotExist(err) { + log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory") + return tmpl, nil + } + return template.New("home").Parse(string(tmplData)) } return tmpl, nil } -var tmpl *template.Template // not used in dev mode, initialized by main after flag parse +// tmpl is the template used in prod mode. +// In dev mode it's only used if the template file doesn't exist on disk. +// It's initialized by main after flag parsing. +var tmpl *template.Template type tmplData struct { DisplayName string // "Foo Barberson" @@ -117,11 +120,6 @@ func root(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) return } - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - http.Error(w, "no remote addr", 500) - return - } tmpl, err := getTmpl() if err != nil { w.Header().Set("Content-Type", "text/plain") @@ -129,7 +127,7 @@ func root(w http.ResponseWriter, r *http.Request) { return } - who, err := whoIs(ip) + who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr) var data tmplData if err != nil { if devMode() { @@ -143,11 +141,12 @@ func root(w http.ResponseWriter, r *http.Request) { IP: "100.1.2.3", } } else { - log.Printf("whois(%q) error: %v", ip, err) + log.Printf("whois(%q) error: %v", r.RemoteAddr, err) http.Error(w, "Your Tailscale works, but we failed to look you up.", 500) return } } else { + ip, _, _ := net.SplitHostPort(r.RemoteAddr) data = tmplData{ DisplayName: who.UserProfile.DisplayName, LoginName: who.UserProfile.LoginName, @@ -168,48 +167,3 @@ func firstLabel(s string) string { } return s } - -// tsSockClient does HTTP requests to the local Tailscale daemon. -// The hostname in the HTTP request is ignored. -var tsSockClient = &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - // On macOS, when dialing from non-sandboxed program to sandboxed GUI running - // a TCP server on a random port, find the random port. For HTTP connections, - // we don't send the token. It gets added in an HTTP Basic-Auth header. - if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil { - var d net.Dialer - return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port)) - } - return safesocket.ConnectDefault() - }, - }, -} - -func whoIs(ip string) (*tailcfg.WhoIsResponse, error) { - ctx := context.Background() - req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil) - if err != nil { - return nil, err - } - if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { - req.SetBasicAuth("", token) - } - res, err := tsSockClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - slurp, _ := ioutil.ReadAll(res.Body) - if res.StatusCode != 200 { - return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp) - } - r := new(tailcfg.WhoIsResponse) - if err := json.Unmarshal(slurp, r); err != nil { - if max := 200; len(slurp) > max { - slurp = slurp[:max] - } - return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp) - } - return r, nil -} From ac3de93d5c025fdbdfebbadbd37fef03ead72d86 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 1 Mar 2021 16:23:22 -0800 Subject: [PATCH 11/48] tempfork/wireguard-windows/firewall: add. This is a fork of wireguard-windows's firewall package, with the firewall rules adjusted to better line up with tailscale's needs. The package was taken from commit 3cc76ed5f222ec82748ef3bd8c41d4b059e28cdb in our fork of wireguard-go. Signed-off-by: David Anderson --- tempfork/wireguard-windows/firewall/README.md | 9 + .../wireguard-windows/firewall/blocker.go | 190 +++ .../wireguard-windows/firewall/helpers.go | 150 +++ .../wireguard-windows/firewall/mksyscall.go | 8 + tempfork/wireguard-windows/firewall/rules.go | 1086 +++++++++++++++++ .../firewall/syscall_windows.go | 36 + .../firewall/types_windows.go | 412 +++++++ .../firewall/types_windows_32.go | 90 ++ .../firewall/types_windows_64.go | 87 ++ .../firewall/types_windows_test.go | 540 ++++++++ .../firewall/zsyscall_windows.go | 132 ++ 11 files changed, 2740 insertions(+) create mode 100644 tempfork/wireguard-windows/firewall/README.md create mode 100644 tempfork/wireguard-windows/firewall/blocker.go create mode 100644 tempfork/wireguard-windows/firewall/helpers.go create mode 100644 tempfork/wireguard-windows/firewall/mksyscall.go create mode 100644 tempfork/wireguard-windows/firewall/rules.go create mode 100644 tempfork/wireguard-windows/firewall/syscall_windows.go create mode 100644 tempfork/wireguard-windows/firewall/types_windows.go create mode 100644 tempfork/wireguard-windows/firewall/types_windows_32.go create mode 100644 tempfork/wireguard-windows/firewall/types_windows_64.go create mode 100644 tempfork/wireguard-windows/firewall/types_windows_test.go create mode 100644 tempfork/wireguard-windows/firewall/zsyscall_windows.go diff --git a/tempfork/wireguard-windows/firewall/README.md b/tempfork/wireguard-windows/firewall/README.md new file mode 100644 index 000000000..a1489425f --- /dev/null +++ b/tempfork/wireguard-windows/firewall/README.md @@ -0,0 +1,9 @@ +This is a copy of the `tunnel/firewall` package of +https://git.zx2c4.com/wireguard-windows, with some hardcoded filter +rules adjusted to function with Tailscale, rather than +wireguard-windows's process structure. + +You should not use this package. It exists as a band-aid while we +figure out how to upstream a more flexible firewall package that does +the fancier things we need, while also supporting wireguard-windows's +goals. diff --git a/tempfork/wireguard-windows/firewall/blocker.go b/tempfork/wireguard-windows/firewall/blocker.go new file mode 100644 index 000000000..e69d791cc --- /dev/null +++ b/tempfork/wireguard-windows/firewall/blocker.go @@ -0,0 +1,190 @@ +// +build windows + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import ( + "errors" + "net" + "unsafe" + + "golang.org/x/sys/windows" +) + +type wfpObjectInstaller func(uintptr) error + +// +// Fundamental WireGuard specific WFP objects. +// +type baseObjects struct { + provider windows.GUID + filters windows.GUID +} + +var wfpSession uintptr + +func createWfpSession() (uintptr, error) { + sessionDisplayData, err := createWtFwpmDisplayData0("WireGuard", "WireGuard dynamic session") + if err != nil { + return 0, wrapErr(err) + } + + session := wtFwpmSession0{ + displayData: *sessionDisplayData, + flags: cFWPM_SESSION_FLAG_DYNAMIC, + txnWaitTimeoutInMSec: windows.INFINITE, + } + + sessionHandle := uintptr(0) + + err = fwpmEngineOpen0(nil, cRPC_C_AUTHN_WINNT, nil, &session, unsafe.Pointer(&sessionHandle)) + if err != nil { + return 0, wrapErr(err) + } + + return sessionHandle, nil +} + +func registerBaseObjects(session uintptr) (*baseObjects, error) { + bo := &baseObjects{} + var err error + bo.provider, err = windows.GenerateGUID() + if err != nil { + return nil, wrapErr(err) + } + bo.filters, err = windows.GenerateGUID() + if err != nil { + return nil, wrapErr(err) + } + + // + // Register provider. + // + { + displayData, err := createWtFwpmDisplayData0("WireGuard", "WireGuard provider") + if err != nil { + return nil, wrapErr(err) + } + provider := wtFwpmProvider0{ + providerKey: bo.provider, + displayData: *displayData, + } + err = fwpmProviderAdd0(session, &provider, 0) + if err != nil { + // TODO: cleanup entire call chain of these if failure? + return nil, wrapErr(err) + } + } + + // + // Register filters sublayer. + // + { + displayData, err := createWtFwpmDisplayData0("WireGuard filters", "Permissive and blocking filters") + if err != nil { + return nil, wrapErr(err) + } + sublayer := wtFwpmSublayer0{ + subLayerKey: bo.filters, + displayData: *displayData, + providerKey: &bo.provider, + weight: ^uint16(0), + } + err = fwpmSubLayerAdd0(session, &sublayer, 0) + if err != nil { + return nil, wrapErr(err) + } + } + + return bo, nil +} + +func EnableFirewall(luid uint64, doNotRestrict bool, restrictToDNSServers []net.IP) error { + if wfpSession != 0 { + return errors.New("The firewall has already been enabled") + } + + session, err := createWfpSession() + if err != nil { + return wrapErr(err) + } + + objectInstaller := func(session uintptr) error { + baseObjects, err := registerBaseObjects(session) + if err != nil { + return wrapErr(err) + } + + err = permitWireGuardService(session, baseObjects, 15) + if err != nil { + return wrapErr(err) + } + + if !doNotRestrict { + err = allowDNS(session, baseObjects) + if err != nil { + return wrapErr(err) + } + + err = permitLoopback(session, baseObjects, 13) + if err != nil { + return wrapErr(err) + } + + err = permitTunInterface(session, baseObjects, 12, luid) + if err != nil { + return wrapErr(err) + } + + err = permitDHCPIPv4(session, baseObjects, 12) + if err != nil { + return wrapErr(err) + } + + err = permitDHCPIPv6(session, baseObjects, 12) + if err != nil { + return wrapErr(err) + } + + err = permitNdp(session, baseObjects, 12) + if err != nil { + return wrapErr(err) + } + + /* TODO: actually evaluate if this does anything and if we need this. It's layer 2; our other rules are layer 3. + * In other words, if somebody complains, try enabling it. For now, keep it off. + err = permitHyperV(session, baseObjects, 12) + if err != nil { + return wrapErr(err) + } + */ + + err = blockAll(session, baseObjects, 0) + if err != nil { + return wrapErr(err) + } + } + + return nil + } + + err = runTransaction(session, objectInstaller) + if err != nil { + fwpmEngineClose0(session) + return wrapErr(err) + } + + wfpSession = session + return nil +} + +func DisableFirewall() { + if wfpSession != 0 { + fwpmEngineClose0(wfpSession) + wfpSession = 0 + } +} diff --git a/tempfork/wireguard-windows/firewall/helpers.go b/tempfork/wireguard-windows/firewall/helpers.go new file mode 100644 index 000000000..1b578e1c4 --- /dev/null +++ b/tempfork/wireguard-windows/firewall/helpers.go @@ -0,0 +1,150 @@ +// +build windows + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import ( + "fmt" + "os" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +func runTransaction(session uintptr, operation wfpObjectInstaller) error { + err := fwpmTransactionBegin0(session, 0) + if err != nil { + return wrapErr(err) + } + + err = operation(session) + if err != nil { + fwpmTransactionAbort0(session) + return wrapErr(err) + } + + err = fwpmTransactionCommit0(session) + if err != nil { + fwpmTransactionAbort0(session) + return wrapErr(err) + } + + return nil +} + +func createWtFwpmDisplayData0(name, description string) (*wtFwpmDisplayData0, error) { + namePtr, err := windows.UTF16PtrFromString(name) + if err != nil { + return nil, wrapErr(err) + } + + descriptionPtr, err := windows.UTF16PtrFromString(description) + if err != nil { + return nil, wrapErr(err) + } + + return &wtFwpmDisplayData0{ + name: namePtr, + description: descriptionPtr, + }, nil +} + +func filterWeight(weight uint8) wtFwpValue0 { + return wtFwpValue0{ + _type: cFWP_UINT8, + value: uintptr(weight), + } +} + +func wrapErr(err error) error { + if _, ok := err.(syscall.Errno); !ok { + return err + } + _, file, line, ok := runtime.Caller(1) + if !ok { + return fmt.Errorf("Firewall error at unknown location: %w", err) + } + return fmt.Errorf("Firewall error at %s:%d: %w", file, line, err) +} + +func getCurrentProcessSecurityDescriptor() (*windows.SECURITY_DESCRIPTOR, error) { + var processToken windows.Token + err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &processToken) + if err != nil { + return nil, wrapErr(err) + } + defer processToken.Close() + gs, err := processToken.GetTokenGroups() + if err != nil { + return nil, wrapErr(err) + } + var sid *windows.SID + for _, g := range gs.AllGroups() { + if g.Attributes != windows.SE_GROUP_ENABLED|windows.SE_GROUP_ENABLED_BY_DEFAULT|windows.SE_GROUP_OWNER { + continue + } + // We could be checking != 6, but hopefully Microsoft will update + // RtlCreateServiceSid to use SHA2, which will then likely bump + // this up. So instead just roll with a minimum. + if !g.Sid.IsValid() || g.Sid.IdentifierAuthority() != windows.SECURITY_NT_AUTHORITY || g.Sid.SubAuthorityCount() < 6 || g.Sid.SubAuthority(0) != 80 { + continue + } + sid = g.Sid + break + } + if sid == nil { + return nil, wrapErr(windows.ERROR_NO_SUCH_GROUP) + } + + access := []windows.EXPLICIT_ACCESS{{ + AccessPermissions: cFWP_ACTRL_MATCH_FILTER, + AccessMode: windows.GRANT_ACCESS, + Trustee: windows.TRUSTEE{ + TrusteeForm: windows.TRUSTEE_IS_SID, + TrusteeType: windows.TRUSTEE_IS_GROUP, + TrusteeValue: windows.TrusteeValueFromSID(sid), + }, + }} + dacl, err := windows.ACLFromEntries(access, nil) + if err != nil { + return nil, wrapErr(err) + } + sd, err := windows.NewSecurityDescriptor() + if err != nil { + return nil, wrapErr(err) + } + err = sd.SetDACL(dacl, true, false) + if err != nil { + return nil, wrapErr(err) + } + sd, err = sd.ToSelfRelative() + if err != nil { + return nil, wrapErr(err) + } + return sd, nil +} + +func getCurrentProcessAppID() (*wtFwpByteBlob, error) { + currentFile, err := os.Executable() + if err != nil { + return nil, wrapErr(err) + } + + curFilePtr, err := windows.UTF16PtrFromString(currentFile) + if err != nil { + return nil, wrapErr(err) + } + + var appID *wtFwpByteBlob + err = fwpmGetAppIdFromFileName0(curFilePtr, unsafe.Pointer(&appID)) + if err != nil { + return nil, wrapErr(err) + } + return appID, nil +} diff --git a/tempfork/wireguard-windows/firewall/mksyscall.go b/tempfork/wireguard-windows/firewall/mksyscall.go new file mode 100644 index 000000000..d5ff98aa2 --- /dev/null +++ b/tempfork/wireguard-windows/firewall/mksyscall.go @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go diff --git a/tempfork/wireguard-windows/firewall/rules.go b/tempfork/wireguard-windows/firewall/rules.go new file mode 100644 index 000000000..72fa20aba --- /dev/null +++ b/tempfork/wireguard-windows/firewall/rules.go @@ -0,0 +1,1086 @@ +// +build windows + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +// +// Known addresses. +// +var ( + linkLocal = wtFwpV6AddrAndMask{[16]uint8{0xfe, 0x80}, 10} + + linkLocalDHCPMulticast = wtFwpByteArray16{[16]uint8{0xFF, 0x02, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x2}} + siteLocalDHCPMulticast = wtFwpByteArray16{[16]uint8{0xFF, 0x05, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x3}} + + linkLocalRouterMulticast = wtFwpByteArray16{[16]uint8{0xFF, 0x02, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2}} +) + +func permitTunInterface(session uintptr, baseObjects *baseObjects, weight uint8, ifLUID uint64) error { + ifaceCondition := wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_IP_LOCAL_INTERFACE, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT64, + value: (uintptr)(unsafe.Pointer(&ifLUID)), + }, + } + + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: 1, + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&ifaceCondition)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + // + // #1 Permit outbound IPv4 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit outbound IPv4 traffic on TUN", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Permit inbound IPv4 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit inbound IPv4 traffic on TUN", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #3 Permit outbound IPv6 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit outbound IPv6 traffic on TUN", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #4 Permit inbound IPv6 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit inbound IPv6 traffic on TUN", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +func permitWireGuardService(session uintptr, baseObjects *baseObjects, weight uint8) error { + var conditions [1]wtFwpmFilterCondition0 + + // + // First condition is the exe path of the current process. + // + appID, err := getCurrentProcessAppID() + if err != nil { + return wrapErr(err) + } + defer fwpmFreeMemory0(unsafe.Pointer(&appID)) + + conditions[0] = wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_ALE_APP_ID, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_BYTE_BLOB_TYPE, + value: uintptr(unsafe.Pointer(appID)), + }, + } + + // + // Assemble the filter. + // + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + flags: cFWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT, + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + // + // #1 Permit outbound IPv4 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit unrestricted outbound traffic for WireGuard service (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Permit inbound IPv4 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit unrestricted inbound traffic for WireGuard service (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #3 Permit outbound IPv6 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit unrestricted outbound traffic for WireGuard service (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #4 Permit inbound IPv6 traffic. + // + { + displayData, err := createWtFwpmDisplayData0("Permit unrestricted inbound traffic for WireGuard service (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +func permitLoopback(session uintptr, baseObjects *baseObjects, weight uint8) error { + condition := wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_FLAGS, + matchType: cFWP_MATCH_FLAGS_ALL_SET, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT32, + value: uintptr(cFWP_CONDITION_FLAG_IS_LOOPBACK), + }, + } + + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: 1, + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&condition)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + // + // #1 Permit outbound IPv4 on loopback. + // + { + displayData, err := createWtFwpmDisplayData0("Permit outbound on loopback (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Permit inbound IPv4 on loopback. + // + { + displayData, err := createWtFwpmDisplayData0("Permit inbound on loopback (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #3 Permit outbound IPv6 on loopback. + // + { + displayData, err := createWtFwpmDisplayData0("Permit outbound on loopback (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #4 Permit inbound IPv6 on loopback. + // + { + displayData, err := createWtFwpmDisplayData0("Permit inbound on loopback (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +func permitDHCPIPv4(session uintptr, baseObjects *baseObjects, weight uint8) error { + // + // #1 Outbound DHCP request on IPv4. + // + { + var conditions [4]wtFwpmFilterCondition0 + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_UDP) + + conditions[1].fieldKey = cFWPM_CONDITION_IP_LOCAL_PORT + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(68) + + conditions[2].fieldKey = cFWPM_CONDITION_IP_REMOTE_PORT + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(67) + + conditions[3].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[3].matchType = cFWP_MATCH_EQUAL + conditions[3].conditionValue._type = cFWP_UINT32 + conditions[3].conditionValue.value = uintptr(0xffffffff) + + displayData, err := createWtFwpmDisplayData0("Permit outbound DHCP request (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter := wtFwpmFilter0{ + displayData: *displayData, + providerKey: &baseObjects.provider, + layerKey: cFWPM_LAYER_ALE_AUTH_CONNECT_V4, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Inbound DHCP response on IPv4. + // + { + var conditions [3]wtFwpmFilterCondition0 + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_UDP) + + conditions[1].fieldKey = cFWPM_CONDITION_IP_LOCAL_PORT + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(68) + + conditions[2].fieldKey = cFWPM_CONDITION_IP_REMOTE_PORT + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(67) + + displayData, err := createWtFwpmDisplayData0("Permit inbound DHCP response (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter := wtFwpmFilter0{ + displayData: *displayData, + providerKey: &baseObjects.provider, + layerKey: cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +func permitDHCPIPv6(session uintptr, baseObjects *baseObjects, weight uint8) error { + // + // #1 Outbound DHCP request on IPv6. + // + { + var conditions [6]wtFwpmFilterCondition0 + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_UDP) + + conditions[1].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_BYTE_ARRAY16_TYPE + conditions[1].conditionValue.value = uintptr(unsafe.Pointer(&linkLocalDHCPMulticast)) + + // Repeat the condition type for logical OR. + conditions[2].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_BYTE_ARRAY16_TYPE + conditions[2].conditionValue.value = uintptr(unsafe.Pointer(&siteLocalDHCPMulticast)) + + conditions[3].fieldKey = cFWPM_CONDITION_IP_REMOTE_PORT + conditions[3].matchType = cFWP_MATCH_EQUAL + conditions[3].conditionValue._type = cFWP_UINT16 + conditions[3].conditionValue.value = uintptr(547) + + conditions[4].fieldKey = cFWPM_CONDITION_IP_LOCAL_ADDRESS + conditions[4].matchType = cFWP_MATCH_EQUAL + conditions[4].conditionValue._type = cFWP_V6_ADDR_MASK + conditions[4].conditionValue.value = uintptr(unsafe.Pointer(&linkLocal)) + + conditions[5].fieldKey = cFWPM_CONDITION_IP_LOCAL_PORT + conditions[5].matchType = cFWP_MATCH_EQUAL + conditions[5].conditionValue._type = cFWP_UINT16 + conditions[5].conditionValue.value = uintptr(546) + + displayData, err := createWtFwpmDisplayData0("Permit outbound DHCP request (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter := wtFwpmFilter0{ + displayData: *displayData, + providerKey: &baseObjects.provider, + layerKey: cFWPM_LAYER_ALE_AUTH_CONNECT_V6, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Inbound DHCP response on IPv6. + // + { + var conditions [5]wtFwpmFilterCondition0 + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_UDP) + + conditions[1].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_V6_ADDR_MASK + conditions[1].conditionValue.value = uintptr(unsafe.Pointer(&linkLocal)) + + conditions[2].fieldKey = cFWPM_CONDITION_IP_REMOTE_PORT + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(547) + + conditions[3].fieldKey = cFWPM_CONDITION_IP_LOCAL_ADDRESS + conditions[3].matchType = cFWP_MATCH_EQUAL + conditions[3].conditionValue._type = cFWP_V6_ADDR_MASK + conditions[3].conditionValue.value = uintptr(unsafe.Pointer(&linkLocal)) + + conditions[4].fieldKey = cFWPM_CONDITION_IP_LOCAL_PORT + conditions[4].matchType = cFWP_MATCH_EQUAL + conditions[4].conditionValue._type = cFWP_UINT16 + conditions[4].conditionValue.value = uintptr(546) + + displayData, err := createWtFwpmDisplayData0("Permit inbound DHCP response (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter := wtFwpmFilter0{ + displayData: *displayData, + providerKey: &baseObjects.provider, + layerKey: cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: uint32(len(conditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&conditions)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +func permitNdp(session uintptr, baseObjects *baseObjects, weight uint8) error { + + /* TODO: actually handle the hop limit somehow! The rules should vaguely be: + * - icmpv6 133: must be outgoing, dst must be FF02::2/128, hop limit must be 255 + * - icmpv6 134: must be incoming, src must be FE80::/10, hop limit must be 255 + * - icmpv6 135: either incoming or outgoing, hop limit must be 255 + * - icmpv6 136: either incoming or outgoing, hop limit must be 255 + * - icmpv6 137: must be incoming, src must be FE80::/10, hop limit must be 255 + */ + + type filterDefinition struct { + displayData *wtFwpmDisplayData0 + conditions []wtFwpmFilterCondition0 + layer windows.GUID + } + + var defs []filterDefinition + + // + // Router Solicitation Message + // ICMP type 133, code 0. Outgoing. + // + { + conditions := make([]wtFwpmFilterCondition0, 4) + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_ICMPV6) + + conditions[1].fieldKey = cFWPM_CONDITION_ICMP_TYPE + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(133) + + conditions[2].fieldKey = cFWPM_CONDITION_ICMP_CODE + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(0) + + conditions[3].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[3].matchType = cFWP_MATCH_EQUAL + conditions[3].conditionValue._type = cFWP_BYTE_ARRAY16_TYPE + conditions[3].conditionValue.value = uintptr(unsafe.Pointer(&linkLocalRouterMulticast)) + + displayData, err := createWtFwpmDisplayData0("Permit NDP type 133", "") + if err != nil { + return wrapErr(err) + } + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_CONNECT_V6, + }) + } + + // + // Router Advertisement Message + // ICMP type 134, code 0. Incoming. + // + { + conditions := make([]wtFwpmFilterCondition0, 4) + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_ICMPV6) + + conditions[1].fieldKey = cFWPM_CONDITION_ICMP_TYPE + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(134) + + conditions[2].fieldKey = cFWPM_CONDITION_ICMP_CODE + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(0) + + conditions[3].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[3].matchType = cFWP_MATCH_EQUAL + conditions[3].conditionValue._type = cFWP_V6_ADDR_MASK + conditions[3].conditionValue.value = uintptr(unsafe.Pointer(&linkLocal)) + + displayData, err := createWtFwpmDisplayData0("Permit NDP type 134", "") + if err != nil { + return wrapErr(err) + } + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6, + }) + } + + // + // Neighbor Solicitation Message + // ICMP type 135, code 0. Bi-directional. + // + { + conditions := make([]wtFwpmFilterCondition0, 3) + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_ICMPV6) + + conditions[1].fieldKey = cFWPM_CONDITION_ICMP_TYPE + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(135) + + conditions[2].fieldKey = cFWPM_CONDITION_ICMP_CODE + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(0) + + displayData, err := createWtFwpmDisplayData0("Permit NDP type 135", "") + if err != nil { + return wrapErr(err) + } + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_CONNECT_V6, + }) + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6, + }) + } + + // + // Neighbor Advertisement Message + // ICMP type 136, code 0. Bi-directional. + // + { + conditions := make([]wtFwpmFilterCondition0, 3) + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_ICMPV6) + + conditions[1].fieldKey = cFWPM_CONDITION_ICMP_TYPE + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(136) + + conditions[2].fieldKey = cFWPM_CONDITION_ICMP_CODE + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(0) + + displayData, err := createWtFwpmDisplayData0("Permit NDP type 136", "") + if err != nil { + return wrapErr(err) + } + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_CONNECT_V6, + }) + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6, + }) + } + + // + // Redirect Message + // ICMP type 137, code 0. Incoming. + // + { + conditions := make([]wtFwpmFilterCondition0, 4) + + conditions[0].fieldKey = cFWPM_CONDITION_IP_PROTOCOL + conditions[0].matchType = cFWP_MATCH_EQUAL + conditions[0].conditionValue._type = cFWP_UINT8 + conditions[0].conditionValue.value = uintptr(cIPPROTO_ICMPV6) + + conditions[1].fieldKey = cFWPM_CONDITION_ICMP_TYPE + conditions[1].matchType = cFWP_MATCH_EQUAL + conditions[1].conditionValue._type = cFWP_UINT16 + conditions[1].conditionValue.value = uintptr(137) + + conditions[2].fieldKey = cFWPM_CONDITION_ICMP_CODE + conditions[2].matchType = cFWP_MATCH_EQUAL + conditions[2].conditionValue._type = cFWP_UINT16 + conditions[2].conditionValue.value = uintptr(0) + + conditions[3].fieldKey = cFWPM_CONDITION_IP_REMOTE_ADDRESS + conditions[3].matchType = cFWP_MATCH_EQUAL + conditions[3].conditionValue._type = cFWP_V6_ADDR_MASK + conditions[3].conditionValue.value = uintptr(unsafe.Pointer(&linkLocal)) + + displayData, err := createWtFwpmDisplayData0("Permit NDP type 137", "") + if err != nil { + return wrapErr(err) + } + + defs = append(defs, filterDefinition{ + displayData: displayData, + conditions: conditions, + layer: cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6, + }) + } + + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + for _, definition := range defs { + filter.displayData = *definition.displayData + filter.layerKey = definition.layer + filter.numFilterConditions = uint32(len(definition.conditions)) + filter.filterCondition = (*wtFwpmFilterCondition0)(unsafe.Pointer(&definition.conditions[0])) + + err := fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +func permitHyperV(session uintptr, baseObjects *baseObjects, weight uint8) error { + // + // Only applicable on Win8+. + // + { + major, minor, _ := windows.RtlGetNtVersionNumbers() + win8plus := major > 6 || (major == 6 && minor >= 3) + + if !win8plus { + return nil + } + } + + condition := wtFwpmFilterCondition0{ + fieldKey: cFWPM_CONDITION_L2_FLAGS, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT32, + value: uintptr(cFWP_CONDITION_L2_IS_VM2VM), + }, + } + + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + numFilterConditions: 1, + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&condition)), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + // + // #1 Outbound. + // + { + displayData, err := createWtFwpmDisplayData0("Permit Hyper-V => Hyper-V outbound", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Inbound. + // + { + displayData, err := createWtFwpmDisplayData0("Permit Hyper-V => Hyper-V inbound", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_INBOUND_MAC_FRAME_NATIVE + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +// Block all traffic except what is explicitly permitted by other rules. +func blockAll(session uintptr, baseObjects *baseObjects, weight uint8) error { + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(weight), + action: wtFwpmAction0{ + _type: cFWP_ACTION_BLOCK, + }, + } + + filterID := uint64(0) + + // + // #1 Block outbound traffic on IPv4. + // + { + displayData, err := createWtFwpmDisplayData0("Block all outbound (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Block inbound traffic on IPv4. + // + { + displayData, err := createWtFwpmDisplayData0("Block all inbound (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #3 Block outbound traffic on IPv6. + // + { + displayData, err := createWtFwpmDisplayData0("Block all outbound (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #4 Block inbound traffic on IPv6. + // + { + displayData, err := createWtFwpmDisplayData0("Block all inbound (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} + +// Allow DNS traffic. This is less ironclad than upstream +// wireguard-windows, but until we rework our DNS configuration +// capability, it's difficult for us to identify the correct DNS +// servers to specifically allow here. +func allowDNS(session uintptr, baseObjects *baseObjects) error { + allowConditions := []wtFwpmFilterCondition0{ + { + fieldKey: cFWPM_CONDITION_IP_REMOTE_PORT, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT16, + value: uintptr(53), + }, + }, + { + fieldKey: cFWPM_CONDITION_IP_PROTOCOL, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT8, + value: uintptr(cIPPROTO_UDP), + }, + }, + // Repeat the condition type for logical OR. + { + fieldKey: cFWPM_CONDITION_IP_PROTOCOL, + matchType: cFWP_MATCH_EQUAL, + conditionValue: wtFwpConditionValue0{ + _type: cFWP_UINT8, + value: uintptr(cIPPROTO_TCP), + }, + }, + } + + filter := wtFwpmFilter0{ + providerKey: &baseObjects.provider, + subLayerKey: baseObjects.filters, + weight: filterWeight(15), + numFilterConditions: uint32(len(allowConditions)), + filterCondition: (*wtFwpmFilterCondition0)(unsafe.Pointer(&allowConditions[0])), + action: wtFwpmAction0{ + _type: cFWP_ACTION_PERMIT, + }, + } + + filterID := uint64(0) + + // + // #1 Allow IPv4 outbound DNS. + // + { + displayData, err := createWtFwpmDisplayData0("Allow DNS outbound (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #2 Allow IPv4 inbound DNS. + // + { + displayData, err := createWtFwpmDisplayData0("Allow DNS inbound (IPv4)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #3 Allow IPv6 outbound DNS. + // + { + displayData, err := createWtFwpmDisplayData0("Allow DNS outbound (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_CONNECT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + // + // #4 Allow IPv6 inbound DNS. + // + { + displayData, err := createWtFwpmDisplayData0("Allow DNS inbound (IPv6)", "") + if err != nil { + return wrapErr(err) + } + + filter.displayData = *displayData + filter.layerKey = cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 + + err = fwpmFilterAdd0(session, &filter, 0, &filterID) + if err != nil { + return wrapErr(err) + } + } + + return nil +} diff --git a/tempfork/wireguard-windows/firewall/syscall_windows.go b/tempfork/wireguard-windows/firewall/syscall_windows.go new file mode 100644 index 000000000..661527d9f --- /dev/null +++ b/tempfork/wireguard-windows/firewall/syscall_windows.go @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineopen0 +//sys fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmEngineOpen0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmengineclose0 +//sys fwpmEngineClose0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmEngineClose0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmsublayeradd0 +//sys fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmSubLayerAdd0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmgetappidfromfilename0 +//sys fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) [failretval!=0] = fwpuclnt.FwpmGetAppIdFromFileName0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfreememory0 +//sys fwpmFreeMemory0(p unsafe.Pointer) = fwpuclnt.FwpmFreeMemory0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmfilteradd0 +//sys fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) [failretval!=0] = fwpuclnt.FwpmFilterAdd0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/Fwpmu/nf-fwpmu-fwpmtransactionbegin0 +//sys fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionBegin0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactioncommit0 +//sys fwpmTransactionCommit0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionCommit0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmtransactionabort0 +//sys fwpmTransactionAbort0(engineHandle uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmTransactionAbort0 + +// https://docs.microsoft.com/en-us/windows/desktop/api/fwpmu/nf-fwpmu-fwpmprovideradd0 +//sys fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) [failretval!=0] = fwpuclnt.FwpmProviderAdd0 diff --git a/tempfork/wireguard-windows/firewall/types_windows.go b/tempfork/wireguard-windows/firewall/types_windows.go new file mode 100644 index 000000000..b0dd1b114 --- /dev/null +++ b/tempfork/wireguard-windows/firewall/types_windows.go @@ -0,0 +1,412 @@ +// +build windows + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import "golang.org/x/sys/windows" + +const ( + anysizeArray = 1 // ANYSIZE_ARRAY defined in winnt.h + + wtFwpBitmapArray64_Size = 8 + + wtFwpByteArray16_Size = 16 + + wtFwpByteArray6_Size = 6 + + wtFwpmAction0_Size = 20 + wtFwpmAction0_filterType_Offset = 4 + + wtFwpV4AddrAndMask_Size = 8 + wtFwpV4AddrAndMask_mask_Offset = 4 + + wtFwpV6AddrAndMask_Size = 17 + wtFwpV6AddrAndMask_prefixLength_Offset = 16 +) + +type wtFwpActionFlag uint32 + +const ( + cFWP_ACTION_FLAG_TERMINATING wtFwpActionFlag = 0x00001000 + cFWP_ACTION_FLAG_NON_TERMINATING wtFwpActionFlag = 0x00002000 + cFWP_ACTION_FLAG_CALLOUT wtFwpActionFlag = 0x00004000 +) + +// FWP_ACTION_TYPE defined in fwptypes.h +type wtFwpActionType uint32 + +const ( + cFWP_ACTION_BLOCK wtFwpActionType = wtFwpActionType(0x00000001 | cFWP_ACTION_FLAG_TERMINATING) + cFWP_ACTION_PERMIT wtFwpActionType = wtFwpActionType(0x00000002 | cFWP_ACTION_FLAG_TERMINATING) + cFWP_ACTION_CALLOUT_TERMINATING wtFwpActionType = wtFwpActionType(0x00000003 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_TERMINATING) + cFWP_ACTION_CALLOUT_INSPECTION wtFwpActionType = wtFwpActionType(0x00000004 | cFWP_ACTION_FLAG_CALLOUT | cFWP_ACTION_FLAG_NON_TERMINATING) + cFWP_ACTION_CALLOUT_UNKNOWN wtFwpActionType = wtFwpActionType(0x00000005 | cFWP_ACTION_FLAG_CALLOUT) + cFWP_ACTION_CONTINUE wtFwpActionType = wtFwpActionType(0x00000006 | cFWP_ACTION_FLAG_NON_TERMINATING) + cFWP_ACTION_NONE wtFwpActionType = 0x00000007 + cFWP_ACTION_NONE_NO_MATCH wtFwpActionType = 0x00000008 + cFWP_ACTION_BITMAP_INDEX_SET wtFwpActionType = 0x00000009 +) + +// FWP_BYTE_BLOB defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_blob_) +type wtFwpByteBlob struct { + size uint32 + data *uint8 +} + +// FWP_MATCH_TYPE defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_match_type_) +type wtFwpMatchType uint32 + +const ( + cFWP_MATCH_EQUAL wtFwpMatchType = 0 + cFWP_MATCH_GREATER wtFwpMatchType = cFWP_MATCH_EQUAL + 1 + cFWP_MATCH_LESS wtFwpMatchType = cFWP_MATCH_GREATER + 1 + cFWP_MATCH_GREATER_OR_EQUAL wtFwpMatchType = cFWP_MATCH_LESS + 1 + cFWP_MATCH_LESS_OR_EQUAL wtFwpMatchType = cFWP_MATCH_GREATER_OR_EQUAL + 1 + cFWP_MATCH_RANGE wtFwpMatchType = cFWP_MATCH_LESS_OR_EQUAL + 1 + cFWP_MATCH_FLAGS_ALL_SET wtFwpMatchType = cFWP_MATCH_RANGE + 1 + cFWP_MATCH_FLAGS_ANY_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ALL_SET + 1 + cFWP_MATCH_FLAGS_NONE_SET wtFwpMatchType = cFWP_MATCH_FLAGS_ANY_SET + 1 + cFWP_MATCH_EQUAL_CASE_INSENSITIVE wtFwpMatchType = cFWP_MATCH_FLAGS_NONE_SET + 1 + cFWP_MATCH_NOT_EQUAL wtFwpMatchType = cFWP_MATCH_EQUAL_CASE_INSENSITIVE + 1 + cFWP_MATCH_PREFIX wtFwpMatchType = cFWP_MATCH_NOT_EQUAL + 1 + cFWP_MATCH_NOT_PREFIX wtFwpMatchType = cFWP_MATCH_PREFIX + 1 + cFWP_MATCH_TYPE_MAX wtFwpMatchType = cFWP_MATCH_NOT_PREFIX + 1 +) + +// FWPM_ACTION0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_action0_) +type wtFwpmAction0 struct { + _type wtFwpActionType + filterType windows.GUID // Windows type: GUID +} + +// Defined in fwpmu.h. 4cd62a49-59c3-4969-b7f3-bda5d32890a4 +var cFWPM_CONDITION_IP_LOCAL_INTERFACE = windows.GUID{ + Data1: 0x4cd62a49, + Data2: 0x59c3, + Data3: 0x4969, + Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4}, +} + +// Defined in fwpmu.h. b235ae9a-1d64-49b8-a44c-5ff3d9095045 +var cFWPM_CONDITION_IP_REMOTE_ADDRESS = windows.GUID{ + Data1: 0xb235ae9a, + Data2: 0x1d64, + Data3: 0x49b8, + Data4: [8]byte{0xa4, 0x4c, 0x5f, 0xf3, 0xd9, 0x09, 0x50, 0x45}, +} + +// Defined in fwpmu.h. 3971ef2b-623e-4f9a-8cb1-6e79b806b9a7 +var cFWPM_CONDITION_IP_PROTOCOL = windows.GUID{ + Data1: 0x3971ef2b, + Data2: 0x623e, + Data3: 0x4f9a, + Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7}, +} + +// Defined in fwpmu.h. 0c1ba1af-5765-453f-af22-a8f791ac775b +var cFWPM_CONDITION_IP_LOCAL_PORT = windows.GUID{ + Data1: 0x0c1ba1af, + Data2: 0x5765, + Data3: 0x453f, + Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b}, +} + +// Defined in fwpmu.h. c35a604d-d22b-4e1a-91b4-68f674ee674b +var cFWPM_CONDITION_IP_REMOTE_PORT = windows.GUID{ + Data1: 0xc35a604d, + Data2: 0xd22b, + Data3: 0x4e1a, + Data4: [8]byte{0x91, 0xb4, 0x68, 0xf6, 0x74, 0xee, 0x67, 0x4b}, +} + +// Defined in fwpmu.h. d78e1e87-8644-4ea5-9437-d809ecefc971 +var cFWPM_CONDITION_ALE_APP_ID = windows.GUID{ + Data1: 0xd78e1e87, + Data2: 0x8644, + Data3: 0x4ea5, + Data4: [8]byte{0x94, 0x37, 0xd8, 0x09, 0xec, 0xef, 0xc9, 0x71}, +} + +// af043a0a-b34d-4f86-979c-c90371af6e66 +var cFWPM_CONDITION_ALE_USER_ID = windows.GUID{ + Data1: 0xaf043a0a, + Data2: 0xb34d, + Data3: 0x4f86, + Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66}, +} + +// d9ee00de-c1ef-4617-bfe3-ffd8f5a08957 +var cFWPM_CONDITION_IP_LOCAL_ADDRESS = windows.GUID{ + Data1: 0xd9ee00de, + Data2: 0xc1ef, + Data3: 0x4617, + Data4: [8]byte{0xbf, 0xe3, 0xff, 0xd8, 0xf5, 0xa0, 0x89, 0x57}, +} + +var cFWPM_CONDITION_ICMP_TYPE = cFWPM_CONDITION_IP_LOCAL_PORT +var cFWPM_CONDITION_ICMP_CODE = cFWPM_CONDITION_IP_REMOTE_PORT + +// 7bc43cbf-37ba-45f1-b74a-82ff518eeb10 +var cFWPM_CONDITION_L2_FLAGS = windows.GUID{ + Data1: 0x7bc43cbf, + Data2: 0x37ba, + Data3: 0x45f1, + Data4: [8]byte{0xb7, 0x4a, 0x82, 0xff, 0x51, 0x8e, 0xeb, 0x10}, +} + +type wtFwpmL2Flags uint32 + +const cFWP_CONDITION_L2_IS_VM2VM wtFwpmL2Flags = 0x00000010 + +var cFWPM_CONDITION_FLAGS = windows.GUID{ + Data1: 0x632ce23b, + Data2: 0x5167, + Data3: 0x435c, + Data4: [8]byte{0x86, 0xd7, 0xe9, 0x03, 0x68, 0x4a, 0xa8, 0x0c}, +} + +type wtFwpmFlags uint32 + +const cFWP_CONDITION_FLAG_IS_LOOPBACK wtFwpmFlags = 0x00000001 + +// Defined in fwpmtypes.h +type wtFwpmFilterFlags uint32 + +const ( + cFWPM_FILTER_FLAG_NONE wtFwpmFilterFlags = 0x00000000 + cFWPM_FILTER_FLAG_PERSISTENT wtFwpmFilterFlags = 0x00000001 + cFWPM_FILTER_FLAG_BOOTTIME wtFwpmFilterFlags = 0x00000002 + cFWPM_FILTER_FLAG_HAS_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000004 + cFWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT wtFwpmFilterFlags = 0x00000008 + cFWPM_FILTER_FLAG_PERMIT_IF_CALLOUT_UNREGISTERED wtFwpmFilterFlags = 0x00000010 + cFWPM_FILTER_FLAG_DISABLED wtFwpmFilterFlags = 0x00000020 + cFWPM_FILTER_FLAG_INDEXED wtFwpmFilterFlags = 0x00000040 + cFWPM_FILTER_FLAG_HAS_SECURITY_REALM_PROVIDER_CONTEXT wtFwpmFilterFlags = 0x00000080 + cFWPM_FILTER_FLAG_SYSTEMOS_ONLY wtFwpmFilterFlags = 0x00000100 + cFWPM_FILTER_FLAG_GAMEOS_ONLY wtFwpmFilterFlags = 0x00000200 + cFWPM_FILTER_FLAG_SILENT_MODE wtFwpmFilterFlags = 0x00000400 + cFWPM_FILTER_FLAG_IPSEC_NO_ACQUIRE_INITIATE wtFwpmFilterFlags = 0x00000800 +) + +// FWPM_LAYER_ALE_AUTH_CONNECT_V4 (c38d57d1-05a7-4c33-904f-7fbceee60e82) defined in fwpmu.h +var cFWPM_LAYER_ALE_AUTH_CONNECT_V4 = windows.GUID{ + Data1: 0xc38d57d1, + Data2: 0x05a7, + Data3: 0x4c33, + Data4: [8]byte{0x90, 0x4f, 0x7f, 0xbc, 0xee, 0xe6, 0x0e, 0x82}, +} + +// e1cd9fe7-f4b5-4273-96c0-592e487b8650 +var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 = windows.GUID{ + Data1: 0xe1cd9fe7, + Data2: 0xf4b5, + Data3: 0x4273, + Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50}, +} + +// FWPM_LAYER_ALE_AUTH_CONNECT_V6 (4a72393b-319f-44bc-84c3-ba54dcb3b6b4) defined in fwpmu.h +var cFWPM_LAYER_ALE_AUTH_CONNECT_V6 = windows.GUID{ + Data1: 0x4a72393b, + Data2: 0x319f, + Data3: 0x44bc, + Data4: [8]byte{0x84, 0xc3, 0xba, 0x54, 0xdc, 0xb3, 0xb6, 0xb4}, +} + +// a3b42c97-9f04-4672-b87e-cee9c483257f +var cFWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 = windows.GUID{ + Data1: 0xa3b42c97, + Data2: 0x9f04, + Data3: 0x4672, + Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f}, +} + +// 94c44912-9d6f-4ebf-b995-05ab8a088d1b +var cFWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE = windows.GUID{ + Data1: 0x94c44912, + Data2: 0x9d6f, + Data3: 0x4ebf, + Data4: [8]byte{0xb9, 0x95, 0x05, 0xab, 0x8a, 0x08, 0x8d, 0x1b}, +} + +// d4220bd3-62ce-4f08-ae88-b56e8526df50 +var cFWPM_LAYER_INBOUND_MAC_FRAME_NATIVE = windows.GUID{ + Data1: 0xd4220bd3, + Data2: 0x62ce, + Data3: 0x4f08, + Data4: [8]byte{0xae, 0x88, 0xb5, 0x6e, 0x85, 0x26, 0xdf, 0x50}, +} + +// FWP_BITMAP_ARRAY64 defined in fwtypes.h +type wtFwpBitmapArray64 struct { + bitmapArray64 [8]uint8 // Windows type: [8]UINT8 +} + +// FWP_BYTE_ARRAY6 defined in fwtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array6_) +type wtFwpByteArray6 struct { + byteArray6 [6]uint8 // Windows type: [6]UINT8 +} + +// FWP_BYTE_ARRAY16 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_byte_array16_) +type wtFwpByteArray16 struct { + byteArray16 [16]uint8 // Windows type [16]UINT8 +} + +// FWP_CONDITION_VALUE0 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_condition_value0). +type wtFwpConditionValue0 wtFwpValue0 + +// FWP_DATA_TYPE defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ne-fwptypes-fwp_data_type_) +type wtFwpDataType uint + +const ( + cFWP_EMPTY wtFwpDataType = 0 + cFWP_UINT8 wtFwpDataType = cFWP_EMPTY + 1 + cFWP_UINT16 wtFwpDataType = cFWP_UINT8 + 1 + cFWP_UINT32 wtFwpDataType = cFWP_UINT16 + 1 + cFWP_UINT64 wtFwpDataType = cFWP_UINT32 + 1 + cFWP_INT8 wtFwpDataType = cFWP_UINT64 + 1 + cFWP_INT16 wtFwpDataType = cFWP_INT8 + 1 + cFWP_INT32 wtFwpDataType = cFWP_INT16 + 1 + cFWP_INT64 wtFwpDataType = cFWP_INT32 + 1 + cFWP_FLOAT wtFwpDataType = cFWP_INT64 + 1 + cFWP_DOUBLE wtFwpDataType = cFWP_FLOAT + 1 + cFWP_BYTE_ARRAY16_TYPE wtFwpDataType = cFWP_DOUBLE + 1 + cFWP_BYTE_BLOB_TYPE wtFwpDataType = cFWP_BYTE_ARRAY16_TYPE + 1 + cFWP_SID wtFwpDataType = cFWP_BYTE_BLOB_TYPE + 1 + cFWP_SECURITY_DESCRIPTOR_TYPE wtFwpDataType = cFWP_SID + 1 + cFWP_TOKEN_INFORMATION_TYPE wtFwpDataType = cFWP_SECURITY_DESCRIPTOR_TYPE + 1 + cFWP_TOKEN_ACCESS_INFORMATION_TYPE wtFwpDataType = cFWP_TOKEN_INFORMATION_TYPE + 1 + cFWP_UNICODE_STRING_TYPE wtFwpDataType = cFWP_TOKEN_ACCESS_INFORMATION_TYPE + 1 + cFWP_BYTE_ARRAY6_TYPE wtFwpDataType = cFWP_UNICODE_STRING_TYPE + 1 + cFWP_BITMAP_INDEX_TYPE wtFwpDataType = cFWP_BYTE_ARRAY6_TYPE + 1 + cFWP_BITMAP_ARRAY64_TYPE wtFwpDataType = cFWP_BITMAP_INDEX_TYPE + 1 + cFWP_SINGLE_DATA_TYPE_MAX wtFwpDataType = 0xff + cFWP_V4_ADDR_MASK wtFwpDataType = cFWP_SINGLE_DATA_TYPE_MAX + 1 + cFWP_V6_ADDR_MASK wtFwpDataType = cFWP_V4_ADDR_MASK + 1 + cFWP_RANGE_TYPE wtFwpDataType = cFWP_V6_ADDR_MASK + 1 + cFWP_DATA_TYPE_MAX wtFwpDataType = cFWP_RANGE_TYPE + 1 +) + +// FWP_V4_ADDR_AND_MASK defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v4_addr_and_mask). +type wtFwpV4AddrAndMask struct { + addr uint32 + mask uint32 +} + +// FWP_V6_ADDR_AND_MASK defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_v6_addr_and_mask). +type wtFwpV6AddrAndMask struct { + addr [16]uint8 + prefixLength uint8 +} + +// FWP_VALUE0 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwp_value0_) +type wtFwpValue0 struct { + _type wtFwpDataType + value uintptr +} + +// FWPM_DISPLAY_DATA0 defined in fwptypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwptypes/ns-fwptypes-fwpm_display_data0). +type wtFwpmDisplayData0 struct { + name *uint16 // Windows type: *wchar_t + description *uint16 // Windows type: *wchar_t +} + +// FWPM_FILTER_CONDITION0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter_condition0). +type wtFwpmFilterCondition0 struct { + fieldKey windows.GUID // Windows type: GUID + matchType wtFwpMatchType + conditionValue wtFwpConditionValue0 +} + +// FWPM_PROVIDER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0_) +type wtFwpProvider0 struct { + providerKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags uint32 + providerData wtFwpByteBlob + serviceName *uint16 // Windows type: *wchar_t +} + +type wtFwpmSessionFlagsValue uint32 + +const ( + cFWPM_SESSION_FLAG_DYNAMIC wtFwpmSessionFlagsValue = 0x00000001 // FWPM_SESSION_FLAG_DYNAMIC defined in fwpmtypes.h +) + +// FWPM_SESSION0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_session0). +type wtFwpmSession0 struct { + sessionKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmSessionFlagsValue // Windows type UINT32 + txnWaitTimeoutInMSec uint32 + processId uint32 // Windows type: DWORD + sid *windows.SID + username *uint16 // Windows type: *wchar_t + kernelMode uint8 // Windows type: BOOL +} + +type wtFwpmSublayerFlags uint32 + +const ( + cFWPM_SUBLAYER_FLAG_PERSISTENT wtFwpmSublayerFlags = 0x00000001 // FWPM_SUBLAYER_FLAG_PERSISTENT defined in fwpmtypes.h +) + +// FWPM_SUBLAYER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_sublayer0_) +type wtFwpmSublayer0 struct { + subLayerKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmSublayerFlags + providerKey *windows.GUID // Windows type: *GUID + providerData wtFwpByteBlob + weight uint16 +} + +// Defined in rpcdce.h +type wtRpcCAuthN uint32 + +const ( + cRPC_C_AUTHN_NONE wtRpcCAuthN = 0 + cRPC_C_AUTHN_WINNT wtRpcCAuthN = 10 + cRPC_C_AUTHN_DEFAULT wtRpcCAuthN = 0xFFFFFFFF +) + +// FWPM_PROVIDER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/sv-se/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_provider0). +type wtFwpmProvider0 struct { + providerKey windows.GUID + displayData wtFwpmDisplayData0 + flags uint32 + providerData wtFwpByteBlob + serviceName *uint16 +} + +type wtIPProto uint32 + +const ( + cIPPROTO_ICMP wtIPProto = 1 + cIPPROTO_ICMPV6 wtIPProto = 58 + cIPPROTO_TCP wtIPProto = 6 + cIPPROTO_UDP wtIPProto = 17 +) + +const ( + cFWP_ACTRL_MATCH_FILTER = 1 +) diff --git a/tempfork/wireguard-windows/firewall/types_windows_32.go b/tempfork/wireguard-windows/firewall/types_windows_32.go new file mode 100644 index 000000000..11a7ab871 --- /dev/null +++ b/tempfork/wireguard-windows/firewall/types_windows_32.go @@ -0,0 +1,90 @@ +// +build windows,386 windows,arm + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import "golang.org/x/sys/windows" + +const ( + wtFwpByteBlob_Size = 8 + wtFwpByteBlob_data_Offset = 4 + + wtFwpConditionValue0_Size = 8 + wtFwpConditionValue0_uint8_Offset = 4 + + wtFwpmDisplayData0_Size = 8 + wtFwpmDisplayData0_description_Offset = 4 + + wtFwpmFilter0_Size = 152 + wtFwpmFilter0_displayData_Offset = 16 + wtFwpmFilter0_flags_Offset = 24 + wtFwpmFilter0_providerKey_Offset = 28 + wtFwpmFilter0_providerData_Offset = 32 + wtFwpmFilter0_layerKey_Offset = 40 + wtFwpmFilter0_subLayerKey_Offset = 56 + wtFwpmFilter0_weight_Offset = 72 + wtFwpmFilter0_numFilterConditions_Offset = 80 + wtFwpmFilter0_filterCondition_Offset = 84 + wtFwpmFilter0_action_Offset = 88 + wtFwpmFilter0_providerContextKey_Offset = 112 + wtFwpmFilter0_reserved_Offset = 128 + wtFwpmFilter0_filterID_Offset = 136 + wtFwpmFilter0_effectiveWeight_Offset = 144 + + wtFwpmFilterCondition0_Size = 28 + wtFwpmFilterCondition0_matchType_Offset = 16 + wtFwpmFilterCondition0_conditionValue_Offset = 20 + + wtFwpmSession0_Size = 48 + wtFwpmSession0_displayData_Offset = 16 + wtFwpmSession0_flags_Offset = 24 + wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 28 + wtFwpmSession0_processId_Offset = 32 + wtFwpmSession0_sid_Offset = 36 + wtFwpmSession0_username_Offset = 40 + wtFwpmSession0_kernelMode_Offset = 44 + + wtFwpmSublayer0_Size = 44 + wtFwpmSublayer0_displayData_Offset = 16 + wtFwpmSublayer0_flags_Offset = 24 + wtFwpmSublayer0_providerKey_Offset = 28 + wtFwpmSublayer0_providerData_Offset = 32 + wtFwpmSublayer0_weight_Offset = 40 + + wtFwpProvider0_Size = 40 + wtFwpProvider0_displayData_Offset = 16 + wtFwpProvider0_flags_Offset = 24 + wtFwpProvider0_providerData_Offset = 28 + wtFwpProvider0_serviceName_Offset = 36 + + wtFwpTokenInformation_Size = 16 + + wtFwpValue0_Size = 8 + wtFwpValue0_value_Offset = 4 +) + +// FWPM_FILTER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0). +type wtFwpmFilter0 struct { + filterKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmFilterFlags + providerKey *windows.GUID // Windows type: *GUID + providerData wtFwpByteBlob + layerKey windows.GUID // Windows type: GUID + subLayerKey windows.GUID // Windows type: GUID + weight wtFwpValue0 + numFilterConditions uint32 + filterCondition *wtFwpmFilterCondition0 + action wtFwpmAction0 + offset1 [4]byte // Layout correction field + providerContextKey windows.GUID // Windows type: GUID + reserved *windows.GUID // Windows type: *GUID + offset2 [4]byte // Layout correction field + filterID uint64 + effectiveWeight wtFwpValue0 +} diff --git a/tempfork/wireguard-windows/firewall/types_windows_64.go b/tempfork/wireguard-windows/firewall/types_windows_64.go new file mode 100644 index 000000000..0cf686e58 --- /dev/null +++ b/tempfork/wireguard-windows/firewall/types_windows_64.go @@ -0,0 +1,87 @@ +// +build windows,amd64 windows,arm64 + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import "golang.org/x/sys/windows" + +const ( + wtFwpByteBlob_Size = 16 + wtFwpByteBlob_data_Offset = 8 + + wtFwpConditionValue0_Size = 16 + wtFwpConditionValue0_uint8_Offset = 8 + + wtFwpmDisplayData0_Size = 16 + wtFwpmDisplayData0_description_Offset = 8 + + wtFwpmFilter0_Size = 200 + wtFwpmFilter0_displayData_Offset = 16 + wtFwpmFilter0_flags_Offset = 32 + wtFwpmFilter0_providerKey_Offset = 40 + wtFwpmFilter0_providerData_Offset = 48 + wtFwpmFilter0_layerKey_Offset = 64 + wtFwpmFilter0_subLayerKey_Offset = 80 + wtFwpmFilter0_weight_Offset = 96 + wtFwpmFilter0_numFilterConditions_Offset = 112 + wtFwpmFilter0_filterCondition_Offset = 120 + wtFwpmFilter0_action_Offset = 128 + wtFwpmFilter0_providerContextKey_Offset = 152 + wtFwpmFilter0_reserved_Offset = 168 + wtFwpmFilter0_filterID_Offset = 176 + wtFwpmFilter0_effectiveWeight_Offset = 184 + + wtFwpmFilterCondition0_Size = 40 + wtFwpmFilterCondition0_matchType_Offset = 16 + wtFwpmFilterCondition0_conditionValue_Offset = 24 + + wtFwpmSession0_Size = 72 + wtFwpmSession0_displayData_Offset = 16 + wtFwpmSession0_flags_Offset = 32 + wtFwpmSession0_txnWaitTimeoutInMSec_Offset = 36 + wtFwpmSession0_processId_Offset = 40 + wtFwpmSession0_sid_Offset = 48 + wtFwpmSession0_username_Offset = 56 + wtFwpmSession0_kernelMode_Offset = 64 + + wtFwpmSublayer0_Size = 72 + wtFwpmSublayer0_displayData_Offset = 16 + wtFwpmSublayer0_flags_Offset = 32 + wtFwpmSublayer0_providerKey_Offset = 40 + wtFwpmSublayer0_providerData_Offset = 48 + wtFwpmSublayer0_weight_Offset = 64 + + wtFwpProvider0_Size = 64 + wtFwpProvider0_displayData_Offset = 16 + wtFwpProvider0_flags_Offset = 32 + wtFwpProvider0_providerData_Offset = 40 + wtFwpProvider0_serviceName_Offset = 56 + + wtFwpValue0_Size = 16 + wtFwpValue0_value_Offset = 8 +) + +// FWPM_FILTER0 defined in fwpmtypes.h +// (https://docs.microsoft.com/en-us/windows/desktop/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0). +type wtFwpmFilter0 struct { + filterKey windows.GUID // Windows type: GUID + displayData wtFwpmDisplayData0 + flags wtFwpmFilterFlags // Windows type: UINT32 + providerKey *windows.GUID // Windows type: *GUID + providerData wtFwpByteBlob + layerKey windows.GUID // Windows type: GUID + subLayerKey windows.GUID // Windows type: GUID + weight wtFwpValue0 + numFilterConditions uint32 + filterCondition *wtFwpmFilterCondition0 + action wtFwpmAction0 + offset1 [4]byte // Layout correction field + providerContextKey windows.GUID // Windows type: GUID + reserved *windows.GUID // Windows type: *GUID + filterID uint64 + effectiveWeight wtFwpValue0 +} diff --git a/tempfork/wireguard-windows/firewall/types_windows_test.go b/tempfork/wireguard-windows/firewall/types_windows_test.go new file mode 100644 index 000000000..baf64a722 --- /dev/null +++ b/tempfork/wireguard-windows/firewall/types_windows_test.go @@ -0,0 +1,540 @@ +// +build windows + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. + */ + +package firewall + +import ( + "testing" + "unsafe" +) + +func TestWtFwpByteBlobSize(t *testing.T) { + + const actualWtFwpByteBlobSize = unsafe.Sizeof(wtFwpByteBlob{}) + + if actualWtFwpByteBlobSize != wtFwpByteBlob_Size { + t.Errorf("Size of FwpByteBlob is %d, although %d is expected.", actualWtFwpByteBlobSize, + wtFwpByteBlob_Size) + } +} + +func TestWtFwpByteBlobOffsets(t *testing.T) { + + s := wtFwpByteBlob{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.data)) - sp + + if offset != wtFwpByteBlob_data_Offset { + t.Errorf("FwpByteBlob.data offset is %d although %d is expected", offset, wtFwpByteBlob_data_Offset) + return + } +} + +func TestWtFwpmAction0Size(t *testing.T) { + + const actualWtFwpmAction0Size = unsafe.Sizeof(wtFwpmAction0{}) + + if actualWtFwpmAction0Size != wtFwpmAction0_Size { + t.Errorf("Size of wtFwpmAction0 is %d, although %d is expected.", actualWtFwpmAction0Size, + wtFwpmAction0_Size) + } +} + +func TestWtFwpmAction0Offsets(t *testing.T) { + + s := wtFwpmAction0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.filterType)) - sp + + if offset != wtFwpmAction0_filterType_Offset { + t.Errorf("wtFwpmAction0.filterType offset is %d although %d is expected", offset, + wtFwpmAction0_filterType_Offset) + return + } +} + +func TestWtFwpBitmapArray64Size(t *testing.T) { + + const actualWtFwpBitmapArray64Size = unsafe.Sizeof(wtFwpBitmapArray64{}) + + if actualWtFwpBitmapArray64Size != wtFwpBitmapArray64_Size { + t.Errorf("Size of wtFwpBitmapArray64 is %d, although %d is expected.", actualWtFwpBitmapArray64Size, + wtFwpBitmapArray64_Size) + } +} + +func TestWtFwpByteArray6Size(t *testing.T) { + + const actualWtFwpByteArray6Size = unsafe.Sizeof(wtFwpByteArray6{}) + + if actualWtFwpByteArray6Size != wtFwpByteArray6_Size { + t.Errorf("Size of wtFwpByteArray6 is %d, although %d is expected.", actualWtFwpByteArray6Size, + wtFwpByteArray6_Size) + } +} + +func TestWtFwpByteArray16Size(t *testing.T) { + + const actualWtFwpByteArray16Size = unsafe.Sizeof(wtFwpByteArray16{}) + + if actualWtFwpByteArray16Size != wtFwpByteArray16_Size { + t.Errorf("Size of wtFwpByteArray16 is %d, although %d is expected.", actualWtFwpByteArray16Size, + wtFwpByteArray16_Size) + } +} + +func TestWtFwpConditionValue0Size(t *testing.T) { + + const actualWtFwpConditionValue0Size = unsafe.Sizeof(wtFwpConditionValue0{}) + + if actualWtFwpConditionValue0Size != wtFwpConditionValue0_Size { + t.Errorf("Size of wtFwpConditionValue0 is %d, although %d is expected.", actualWtFwpConditionValue0Size, + wtFwpConditionValue0_Size) + } +} + +func TestWtFwpConditionValue0Offsets(t *testing.T) { + + s := wtFwpConditionValue0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.value)) - sp + + if offset != wtFwpConditionValue0_uint8_Offset { + t.Errorf("wtFwpConditionValue0.value offset is %d although %d is expected", offset, wtFwpConditionValue0_uint8_Offset) + return + } +} + +func TestWtFwpV4AddrAndMaskSize(t *testing.T) { + + const actualWtFwpV4AddrAndMaskSize = unsafe.Sizeof(wtFwpV4AddrAndMask{}) + + if actualWtFwpV4AddrAndMaskSize != wtFwpV4AddrAndMask_Size { + t.Errorf("Size of wtFwpV4AddrAndMask is %d, although %d is expected.", actualWtFwpV4AddrAndMaskSize, + wtFwpV4AddrAndMask_Size) + } +} + +func TestWtFwpV4AddrAndMaskOffsets(t *testing.T) { + + s := wtFwpV4AddrAndMask{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.mask)) - sp + + if offset != wtFwpV4AddrAndMask_mask_Offset { + t.Errorf("wtFwpV4AddrAndMask.mask offset is %d although %d is expected", offset, + wtFwpV4AddrAndMask_mask_Offset) + return + } +} + +func TestWtFwpV6AddrAndMaskSize(t *testing.T) { + + const actualWtFwpV6AddrAndMaskSize = unsafe.Sizeof(wtFwpV6AddrAndMask{}) + + if actualWtFwpV6AddrAndMaskSize != wtFwpV6AddrAndMask_Size { + t.Errorf("Size of wtFwpV6AddrAndMask is %d, although %d is expected.", actualWtFwpV6AddrAndMaskSize, + wtFwpV6AddrAndMask_Size) + } +} + +func TestWtFwpV6AddrAndMaskOffsets(t *testing.T) { + + s := wtFwpV6AddrAndMask{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.prefixLength)) - sp + + if offset != wtFwpV6AddrAndMask_prefixLength_Offset { + t.Errorf("wtFwpV6AddrAndMask.prefixLength offset is %d although %d is expected", offset, + wtFwpV6AddrAndMask_prefixLength_Offset) + return + } +} + +func TestWtFwpValue0Size(t *testing.T) { + + const actualWtFwpValue0Size = unsafe.Sizeof(wtFwpValue0{}) + + if actualWtFwpValue0Size != wtFwpValue0_Size { + t.Errorf("Size of wtFwpValue0 is %d, although %d is expected.", actualWtFwpValue0Size, wtFwpValue0_Size) + } +} + +func TestWtFwpValue0Offsets(t *testing.T) { + + s := wtFwpValue0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.value)) - sp + + if offset != wtFwpValue0_value_Offset { + t.Errorf("wtFwpValue0.value offset is %d although %d is expected", offset, wtFwpValue0_value_Offset) + return + } +} + +func TestWtFwpmDisplayData0Size(t *testing.T) { + + const actualWtFwpmDisplayData0Size = unsafe.Sizeof(wtFwpmDisplayData0{}) + + if actualWtFwpmDisplayData0Size != wtFwpmDisplayData0_Size { + t.Errorf("Size of wtFwpmDisplayData0 is %d, although %d is expected.", actualWtFwpmDisplayData0Size, + wtFwpmDisplayData0_Size) + } +} + +func TestWtFwpmDisplayData0Offsets(t *testing.T) { + + s := wtFwpmDisplayData0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.description)) - sp + + if offset != wtFwpmDisplayData0_description_Offset { + t.Errorf("wtFwpmDisplayData0.description offset is %d although %d is expected", offset, + wtFwpmDisplayData0_description_Offset) + return + } +} + +func TestWtFwpmFilterCondition0Size(t *testing.T) { + + const actualWtFwpmFilterCondition0Size = unsafe.Sizeof(wtFwpmFilterCondition0{}) + + if actualWtFwpmFilterCondition0Size != wtFwpmFilterCondition0_Size { + t.Errorf("Size of wtFwpmFilterCondition0 is %d, although %d is expected.", + actualWtFwpmFilterCondition0Size, wtFwpmFilterCondition0_Size) + } +} + +func TestWtFwpmFilterCondition0Offsets(t *testing.T) { + + s := wtFwpmFilterCondition0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.matchType)) - sp + + if offset != wtFwpmFilterCondition0_matchType_Offset { + t.Errorf("wtFwpmFilterCondition0.matchType offset is %d although %d is expected", offset, + wtFwpmFilterCondition0_matchType_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.conditionValue)) - sp + + if offset != wtFwpmFilterCondition0_conditionValue_Offset { + t.Errorf("wtFwpmFilterCondition0.conditionValue offset is %d although %d is expected", offset, + wtFwpmFilterCondition0_conditionValue_Offset) + return + } +} + +func TestWtFwpmFilter0Size(t *testing.T) { + + const actualWtFwpmFilter0Size = unsafe.Sizeof(wtFwpmFilter0{}) + + if actualWtFwpmFilter0Size != wtFwpmFilter0_Size { + t.Errorf("Size of wtFwpmFilter0 is %d, although %d is expected.", actualWtFwpmFilter0Size, + wtFwpmFilter0_Size) + } +} + +func TestWtFwpmFilter0Offsets(t *testing.T) { + + s := wtFwpmFilter0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.displayData)) - sp + + if offset != wtFwpmFilter0_displayData_Offset { + t.Errorf("wtFwpmFilter0.displayData offset is %d although %d is expected", offset, + wtFwpmFilter0_displayData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.flags)) - sp + + if offset != wtFwpmFilter0_flags_Offset { + t.Errorf("wtFwpmFilter0.flags offset is %d although %d is expected", offset, wtFwpmFilter0_flags_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.providerKey)) - sp + + if offset != wtFwpmFilter0_providerKey_Offset { + t.Errorf("wtFwpmFilter0.providerKey offset is %d although %d is expected", offset, + wtFwpmFilter0_providerKey_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.providerData)) - sp + + if offset != wtFwpmFilter0_providerData_Offset { + t.Errorf("wtFwpmFilter0.providerData offset is %d although %d is expected", offset, + wtFwpmFilter0_providerData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.layerKey)) - sp + + if offset != wtFwpmFilter0_layerKey_Offset { + t.Errorf("wtFwpmFilter0.layerKey offset is %d although %d is expected", offset, + wtFwpmFilter0_layerKey_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.subLayerKey)) - sp + + if offset != wtFwpmFilter0_subLayerKey_Offset { + t.Errorf("wtFwpmFilter0.subLayerKey offset is %d although %d is expected", offset, + wtFwpmFilter0_subLayerKey_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.weight)) - sp + + if offset != wtFwpmFilter0_weight_Offset { + t.Errorf("wtFwpmFilter0.weight offset is %d although %d is expected", offset, + wtFwpmFilter0_weight_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.numFilterConditions)) - sp + + if offset != wtFwpmFilter0_numFilterConditions_Offset { + t.Errorf("wtFwpmFilter0.numFilterConditions offset is %d although %d is expected", offset, + wtFwpmFilter0_numFilterConditions_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.filterCondition)) - sp + + if offset != wtFwpmFilter0_filterCondition_Offset { + t.Errorf("wtFwpmFilter0.filterCondition offset is %d although %d is expected", offset, + wtFwpmFilter0_filterCondition_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.action)) - sp + + if offset != wtFwpmFilter0_action_Offset { + t.Errorf("wtFwpmFilter0.action offset is %d although %d is expected", offset, + wtFwpmFilter0_action_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.providerContextKey)) - sp + + if offset != wtFwpmFilter0_providerContextKey_Offset { + t.Errorf("wtFwpmFilter0.providerContextKey offset is %d although %d is expected", offset, + wtFwpmFilter0_providerContextKey_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.reserved)) - sp + + if offset != wtFwpmFilter0_reserved_Offset { + t.Errorf("wtFwpmFilter0.reserved offset is %d although %d is expected", offset, + wtFwpmFilter0_reserved_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.filterID)) - sp + + if offset != wtFwpmFilter0_filterID_Offset { + t.Errorf("wtFwpmFilter0.filterID offset is %d although %d is expected", offset, + wtFwpmFilter0_filterID_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.effectiveWeight)) - sp + + if offset != wtFwpmFilter0_effectiveWeight_Offset { + t.Errorf("wtFwpmFilter0.effectiveWeight offset is %d although %d is expected", offset, + wtFwpmFilter0_effectiveWeight_Offset) + return + } +} + +func TestWtFwpProvider0Size(t *testing.T) { + + const actualWtFwpProvider0Size = unsafe.Sizeof(wtFwpProvider0{}) + + if actualWtFwpProvider0Size != wtFwpProvider0_Size { + t.Errorf("Size of wtFwpProvider0 is %d, although %d is expected.", actualWtFwpProvider0Size, + wtFwpProvider0_Size) + } +} + +func TestWtFwpProvider0Offsets(t *testing.T) { + + s := wtFwpProvider0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.displayData)) - sp + + if offset != wtFwpProvider0_displayData_Offset { + t.Errorf("wtFwpProvider0.displayData offset is %d although %d is expected", offset, + wtFwpProvider0_displayData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.flags)) - sp + + if offset != wtFwpProvider0_flags_Offset { + t.Errorf("wtFwpProvider0.flags offset is %d although %d is expected", offset, + wtFwpProvider0_flags_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.providerData)) - sp + + if offset != wtFwpProvider0_providerData_Offset { + t.Errorf("wtFwpProvider0.providerData offset is %d although %d is expected", offset, + wtFwpProvider0_providerData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.serviceName)) - sp + + if offset != wtFwpProvider0_serviceName_Offset { + t.Errorf("wtFwpProvider0.serviceName offset is %d although %d is expected", offset, + wtFwpProvider0_serviceName_Offset) + return + } +} + +func TestWtFwpmSession0Size(t *testing.T) { + + const actualWtFwpmSession0Size = unsafe.Sizeof(wtFwpmSession0{}) + + if actualWtFwpmSession0Size != wtFwpmSession0_Size { + t.Errorf("Size of wtFwpmSession0 is %d, although %d is expected.", actualWtFwpmSession0Size, + wtFwpmSession0_Size) + } +} + +func TestWtFwpmSession0Offsets(t *testing.T) { + + s := wtFwpmSession0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.displayData)) - sp + + if offset != wtFwpmSession0_displayData_Offset { + t.Errorf("wtFwpmSession0.displayData offset is %d although %d is expected", offset, + wtFwpmSession0_displayData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.flags)) - sp + + if offset != wtFwpmSession0_flags_Offset { + t.Errorf("wtFwpmSession0.flags offset is %d although %d is expected", offset, wtFwpmSession0_flags_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.txnWaitTimeoutInMSec)) - sp + + if offset != wtFwpmSession0_txnWaitTimeoutInMSec_Offset { + t.Errorf("wtFwpmSession0.txnWaitTimeoutInMSec offset is %d although %d is expected", offset, + wtFwpmSession0_txnWaitTimeoutInMSec_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.processId)) - sp + + if offset != wtFwpmSession0_processId_Offset { + t.Errorf("wtFwpmSession0.processId offset is %d although %d is expected", offset, + wtFwpmSession0_processId_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.sid)) - sp + + if offset != wtFwpmSession0_sid_Offset { + t.Errorf("wtFwpmSession0.sid offset is %d although %d is expected", offset, wtFwpmSession0_sid_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.username)) - sp + + if offset != wtFwpmSession0_username_Offset { + t.Errorf("wtFwpmSession0.username offset is %d although %d is expected", offset, + wtFwpmSession0_username_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.kernelMode)) - sp + + if offset != wtFwpmSession0_kernelMode_Offset { + t.Errorf("wtFwpmSession0.kernelMode offset is %d although %d is expected", offset, + wtFwpmSession0_kernelMode_Offset) + return + } +} + +func TestWtFwpmSublayer0Size(t *testing.T) { + + const actualWtFwpmSublayer0Size = unsafe.Sizeof(wtFwpmSublayer0{}) + + if actualWtFwpmSublayer0Size != wtFwpmSublayer0_Size { + t.Errorf("Size of wtFwpmSublayer0 is %d, although %d is expected.", actualWtFwpmSublayer0Size, + wtFwpmSublayer0_Size) + } +} + +func TestWtFwpmSublayer0Offsets(t *testing.T) { + + s := wtFwpmSublayer0{} + sp := uintptr(unsafe.Pointer(&s)) + + offset := uintptr(unsafe.Pointer(&s.displayData)) - sp + + if offset != wtFwpmSublayer0_displayData_Offset { + t.Errorf("wtFwpmSublayer0.displayData offset is %d although %d is expected", offset, + wtFwpmSublayer0_displayData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.flags)) - sp + + if offset != wtFwpmSublayer0_flags_Offset { + t.Errorf("wtFwpmSublayer0.flags offset is %d although %d is expected", offset, + wtFwpmSublayer0_flags_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.providerKey)) - sp + + if offset != wtFwpmSublayer0_providerKey_Offset { + t.Errorf("wtFwpmSublayer0.providerKey offset is %d although %d is expected", offset, + wtFwpmSublayer0_providerKey_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.providerData)) - sp + + if offset != wtFwpmSublayer0_providerData_Offset { + t.Errorf("wtFwpmSublayer0.providerData offset is %d although %d is expected", offset, + wtFwpmSublayer0_providerData_Offset) + return + } + + offset = uintptr(unsafe.Pointer(&s.weight)) - sp + + if offset != wtFwpmSublayer0_weight_Offset { + t.Errorf("wtFwpmSublayer0.weight offset is %d although %d is expected", offset, + wtFwpmSublayer0_weight_Offset) + return + } +} diff --git a/tempfork/wireguard-windows/firewall/zsyscall_windows.go b/tempfork/wireguard-windows/firewall/zsyscall_windows.go new file mode 100644 index 000000000..781a8d38e --- /dev/null +++ b/tempfork/wireguard-windows/firewall/zsyscall_windows.go @@ -0,0 +1,132 @@ +// +build windows + +// Code generated by 'go generate'; DO NOT EDIT. + +package firewall + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modfwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll") + + procFwpmEngineClose0 = modfwpuclnt.NewProc("FwpmEngineClose0") + procFwpmEngineOpen0 = modfwpuclnt.NewProc("FwpmEngineOpen0") + procFwpmFilterAdd0 = modfwpuclnt.NewProc("FwpmFilterAdd0") + procFwpmFreeMemory0 = modfwpuclnt.NewProc("FwpmFreeMemory0") + procFwpmGetAppIdFromFileName0 = modfwpuclnt.NewProc("FwpmGetAppIdFromFileName0") + procFwpmProviderAdd0 = modfwpuclnt.NewProc("FwpmProviderAdd0") + procFwpmSubLayerAdd0 = modfwpuclnt.NewProc("FwpmSubLayerAdd0") + procFwpmTransactionAbort0 = modfwpuclnt.NewProc("FwpmTransactionAbort0") + procFwpmTransactionBegin0 = modfwpuclnt.NewProc("FwpmTransactionBegin0") + procFwpmTransactionCommit0 = modfwpuclnt.NewProc("FwpmTransactionCommit0") +) + +func fwpmEngineClose0(engineHandle uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmEngineClose0.Addr(), 1, uintptr(engineHandle), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmEngineOpen0(serverName *uint16, authnService wtRpcCAuthN, authIdentity *uintptr, session *wtFwpmSession0, engineHandle unsafe.Pointer) (err error) { + r1, _, e1 := syscall.Syscall6(procFwpmEngineOpen0.Addr(), 5, uintptr(unsafe.Pointer(serverName)), uintptr(authnService), uintptr(unsafe.Pointer(authIdentity)), uintptr(unsafe.Pointer(session)), uintptr(engineHandle), 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmFilterAdd0(engineHandle uintptr, filter *wtFwpmFilter0, sd uintptr, id *uint64) (err error) { + r1, _, e1 := syscall.Syscall6(procFwpmFilterAdd0.Addr(), 4, uintptr(engineHandle), uintptr(unsafe.Pointer(filter)), uintptr(sd), uintptr(unsafe.Pointer(id)), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmFreeMemory0(p unsafe.Pointer) { + syscall.Syscall(procFwpmFreeMemory0.Addr(), 1, uintptr(p), 0, 0) + return +} + +func fwpmGetAppIdFromFileName0(fileName *uint16, appID unsafe.Pointer) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmGetAppIdFromFileName0.Addr(), 2, uintptr(unsafe.Pointer(fileName)), uintptr(appID), 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmProviderAdd0(engineHandle uintptr, provider *wtFwpmProvider0, sd uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmProviderAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(provider)), uintptr(sd)) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmSubLayerAdd0(engineHandle uintptr, subLayer *wtFwpmSublayer0, sd uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmSubLayerAdd0.Addr(), 3, uintptr(engineHandle), uintptr(unsafe.Pointer(subLayer)), uintptr(sd)) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmTransactionAbort0(engineHandle uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmTransactionAbort0.Addr(), 1, uintptr(engineHandle), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmTransactionBegin0(engineHandle uintptr, flags uint32) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmTransactionBegin0.Addr(), 2, uintptr(engineHandle), uintptr(flags), 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} + +func fwpmTransactionCommit0(engineHandle uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procFwpmTransactionCommit0.Addr(), 1, uintptr(engineHandle), 0, 0) + if r1 != 0 { + err = errnoErr(e1) + } + return +} From 793cb131f0a04c0a579684ce2dbf4301f0366925 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 1 Mar 2021 16:24:26 -0800 Subject: [PATCH 12/48] wgengine/router: toggle killswitch when using default routes on windows. Fixes #1398. Signed-off-by: David Anderson --- cmd/tailscaled/depaware.txt | 1 + cmd/tailscaled/tailscaled_windows.go | 45 +++++ wgengine/router/router_windows.go | 251 +++++++++++++++++++-------- 3 files changed, 225 insertions(+), 72 deletions(-) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 425cb1641..c07cbbb08 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -108,6 +108,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ tailscale.com/syncs from tailscale.com/net/interfaces+ tailscale.com/tailcfg from tailscale.com/control/controlclient+ + W 💣 tailscale.com/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled W tailscale.com/tsconst from tailscale.com/net/interfaces tailscale.com/tstime from tailscale.com/wgengine/magicsock tailscale.com/types/empty from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index 342f0c237..cf97bef4a 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -21,13 +21,16 @@ import ( "context" "fmt" "log" + "net" "os" "time" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" + "tailscale.com/tempfork/wireguard-windows/firewall" "tailscale.com/types/logger" "tailscale.com/version" "tailscale.com/wgengine" @@ -83,6 +86,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch } func beWindowsSubprocess() bool { + if beFirewallKillswitch() { + return true + } + if len(os.Args) != 3 || os.Args[1] != "/subproc" { return false } @@ -108,6 +115,44 @@ func beWindowsSubprocess() bool { return true } +func beFirewallKillswitch() bool { + if len(os.Args) != 3 || os.Args[1] != "/firewall" { + return false + } + + 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) + } + + luid, err := winipcfg.LUIDFromGUID(&guid) + if err != nil { + log.Fatalf("no interface with GUID %q", guid) + } + + noProtection := false + var dnsIPs []net.IP // unused in called code. + start := time.Now() + firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs) + log.Printf("killswitch enabled, took %s", time.Since(start)) + + // Block until the monitor goroutine shuts us down. + select {} +} + func startIPNServer(ctx context.Context, logid string) error { var logf logger.Logf = log.Printf var eng wgengine.Engine diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go index b600709d3..0f1ed8438 100644 --- a/wgengine/router/router_windows.go +++ b/wgengine/router/router_windows.go @@ -5,17 +5,22 @@ package router import ( + "bufio" "context" "fmt" + "io" "os" "os/exec" + "strings" "sync" "syscall" "time" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" + "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" + "inet.af/netaddr" "tailscale.com/logtail/backoff" "tailscale.com/types/logger" "tailscale.com/wgengine/router/dns" @@ -29,6 +34,15 @@ type winRouter struct { routeChangeCallback *winipcfg.RouteChangeCallback dns *dns.Manager firewall *firewallTweaker + + // firewallSubproc is a subprocess that runs a tweaked version of + // wireguard-windows's "default route killswitch" code. We run it + // as a subprocess because it does unsafe callouts to the WFP API, + // and we want to defend against memory corruption in our main + // process. Owned and mutated only by Set, and doesn't need a lock + // because Set is only called with wgengine's lock held, + // preventing concurrent reconfigs. + firewallSubproc *exec.Cmd } func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) { @@ -55,7 +69,10 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic tunname: tunname, nativeTun: nativeTun, dns: dns.NewManager(mconfig), - firewall: &firewallTweaker{logf: logger.WithPrefix(logf, "firewall: ")}, + firewall: &firewallTweaker{ + logf: logger.WithPrefix(logf, "firewall: "), + tunGUID: *guid, + }, }, nil } @@ -82,7 +99,7 @@ func (r *winRouter) Set(cfg *Config) error { for _, la := range cfg.LocalAddrs { localAddrs = append(localAddrs, la.String()) } - r.firewall.set(localAddrs) + r.firewall.set(localAddrs, cfg.Routes) err := configureInterface(cfg, r.nativeTun) if err != nil { @@ -97,6 +114,15 @@ func (r *winRouter) Set(cfg *Config) error { return nil } +func hasDefaultRoute(routes []netaddr.IPPrefix) bool { + for _, route := range routes { + if route.Bits == 0 { + return true + } + } + return false +} + func (r *winRouter) Close() error { r.firewall.clear() @@ -120,23 +146,36 @@ func cleanup(logf logger.Logf, interfaceName string) { // See https://github.com/tailscale/tailscale/issues/785. // So this tracks the desired state and runs the actual adjusting code asynchrounsly. type firewallTweaker struct { - logf logger.Logf + 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) - want []string // next value we want, or "" to delete the firewall rule - lastVal []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 + wantKillswitch bool + lastKillswitch bool + + // Only touched by doAsyncSet, so mu doesn't need to be held. + + // fwProc is a subprocess that runs the wireguard-windows firewall + // killswitch code. It is only non-nil when the default route + // killswitch is active, and may go back and forth between nil and + // non-nil any number of times during the process's lifetime. + fwProc *exec.Cmd + // stop makes fwProc exit when closed. + stop io.Closer } -func (ft *firewallTweaker) clear() { ft.set(nil) } +func (ft *firewallTweaker) clear() { ft.set(nil, nil) } -// set takes the IPv4 and/or IPv6 CIDRs to allow; an empty slice -// removes the firwall rules. +// set takes CIDRs to allow, and the routes that point into the Tailscale tun interface. +// Empty slices remove firewall rules. // -// set takes ownership of the slice. -func (ft *firewallTweaker) set(cidrs []string) { +// set takes ownership of cidrs, but not routes. +func (ft *firewallTweaker) set(cidrs []string, routes []netaddr.IPPrefix) { ft.mu.Lock() defer ft.mu.Unlock() @@ -145,9 +184,10 @@ func (ft *firewallTweaker) set(cidrs []string) { } else { ft.logf("marking allowed %v", cidrs) } - ft.want = cidrs + ft.wantLocal = cidrs + ft.wantKillswitch = hasDefaultRoute(routes) if ft.running { - // The doAsyncSet goroutine will check ft.want + // The doAsyncSet goroutine will check ft.wantLocal/wantKillswitch // before returning. return } @@ -171,77 +211,144 @@ func (ft *firewallTweaker) doAsyncSet() { ft.mu.Lock() for { // invariant: ft.mu must be locked when beginning this block - val := ft.want - if ft.known && strsEqual(ft.lastVal, val) { + val := ft.wantLocal + if ft.known && strsEqual(ft.lastLocal, val) && ft.wantKillswitch == ft.lastKillswitch { ft.running = false ft.logf("ending netsh goroutine") ft.mu.Unlock() return } - needClear := !ft.known || len(ft.lastVal) > 0 || len(val) == 0 + wantKillswitch := ft.wantKillswitch + needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0 needProcRule := !ft.didProcRule ft.mu.Unlock() - if needClear { - ft.logf("clearing Tailscale-In firewall rules...") - // We ignore the error here, because netsh returns an error for - // deleting something that doesn't match. - // TODO(bradfitz): care? That'd involve querying it before/after to see - // whether it was necessary/worked. But the output format is localized, - // so can't rely on parsing English. Maybe need to use OLE, not netsh.exe? - d, _ := ft.runFirewall("delete", "rule", "name=Tailscale-In", "dir=in") - ft.logf("cleared Tailscale-In firewall rules in %v", d) - } - if needProcRule { - ft.logf("deleting any prior Tailscale-Process rule...") - d, err := ft.runFirewall("delete", "rule", "name=Tailscale-Process", "dir=in") // best effort - if err == nil { - ft.logf("removed old Tailscale-Process rule in %v", d) - } - var exe string - exe, err = os.Executable() - if err != nil { - ft.logf("failed to find Executable for Tailscale-Process rule: %v", err) - } else { - ft.logf("adding Tailscale-Process rule to allow UDP for %q ...", exe) - d, err = ft.runFirewall("add", "rule", "name=Tailscale-Process", - "dir=in", - "action=allow", - "edge=yes", - "program="+exe, - "protocol=udp", - "profile=any", - "enable=yes", - ) - if err != nil { - ft.logf("error adding Tailscale-Process rule: %v", err) - } else { - ft.mu.Lock() - ft.didProcRule = true - ft.mu.Unlock() - ft.logf("added Tailscale-Process rule in %v", d) - } - } - } - var err error - for _, cidr := range val { - ft.logf("adding Tailscale-In rule to allow %v ...", cidr) - var d time.Duration - d, err = ft.runFirewall("add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+cidr, "profile=private", "enable=yes") - if err != nil { - ft.logf("error adding Tailscale-In rule to allow %v: %v", cidr, err) - break - } - ft.logf("added Tailscale-In rule to allow %v in %v", cidr, d) - } + err := ft.doSet(val, wantKillswitch, needClear, needProcRule) bo.BackOff(ctx, err) ft.mu.Lock() - ft.lastVal = val + ft.lastLocal = val + ft.lastKillswitch = wantKillswitch ft.known = (err == nil) } } +// doSet creates and deletes firewall rules to make the system state +// match the values of local, killswitch, clear and procRule. +// +// local is the list of local Tailscale addresses (formatted as CIDR +// prefixes) to allow through the Windows firewall. +// killswitch, if true, enables the wireguard-windows based internet +// killswitch to prevent use of non-Tailscale default routes. +// clear, if true, removes all tailscale address firewall rules before +// adding local. +// procRule, if true, installs a firewall rule that permits the Tailscale +// 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 { + if clear { + ft.logf("clearing Tailscale-In firewall rules...") + // We ignore the error here, because netsh returns an error for + // deleting something that doesn't match. + // TODO(bradfitz): care? That'd involve querying it before/after to see + // whether it was necessary/worked. But the output format is localized, + // so can't rely on parsing English. Maybe need to use OLE, not netsh.exe? + d, _ := ft.runFirewall("delete", "rule", "name=Tailscale-In", "dir=in") + ft.logf("cleared Tailscale-In firewall rules in %v", d) + } + if procRule { + ft.logf("deleting any prior Tailscale-Process rule...") + d, err := ft.runFirewall("delete", "rule", "name=Tailscale-Process", "dir=in") // best effort + if err == nil { + ft.logf("removed old Tailscale-Process rule in %v", d) + } + var exe string + exe, err = os.Executable() + if err != nil { + ft.logf("failed to find Executable for Tailscale-Process rule: %v", err) + } else { + ft.logf("adding Tailscale-Process rule to allow UDP for %q ...", exe) + d, err = ft.runFirewall("add", "rule", "name=Tailscale-Process", + "dir=in", + "action=allow", + "edge=yes", + "program="+exe, + "protocol=udp", + "profile=any", + "enable=yes", + ) + if err != nil { + ft.logf("error adding Tailscale-Process rule: %v", err) + } else { + ft.mu.Lock() + ft.didProcRule = true + ft.mu.Unlock() + ft.logf("added Tailscale-Process rule in %v", d) + } + } + } + for _, cidr := range local { + ft.logf("adding Tailscale-In rule to allow %v ...", cidr) + var d time.Duration + d, err := ft.runFirewall("add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+cidr, "profile=private", "enable=yes") + if err != nil { + ft.logf("error adding Tailscale-In rule to allow %v: %v", cidr, err) + return err + } + ft.logf("added Tailscale-In rule to allow %v in %v", cidr, d) + } + + if killswitch && 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() + if err != nil { + return err + } + proc.Stderr = proc.Stdout + in, err = proc.StdinPipe() + if err != nil { + out.Close() + return err + } + + go func(out io.ReadCloser) { + b := bufio.NewReaderSize(out, 1<<10) + for { + line, err := b.ReadString('\n') + if err != nil { + return + } + line = strings.TrimSpace(line) + if line != "" { + ft.logf("fw-child: %s", line) + } + } + }(out) + + if err := proc.Start(); err != nil { + return err + } + ft.stop = in + ft.fwProc = proc + } else if !killswitch && ft.fwProc != nil { + ft.stop.Close() + ft.stop = nil + ft.fwProc.Wait() + ft.fwProc = nil + } + + return nil +} + func strsEqual(a, b []string) bool { if len(a) != len(b) { return false From 487c520109be97fc9b82d75dd1a8293eeed026a1 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 20:20:25 -0800 Subject: [PATCH 13/48] wgengine: fix bug from earlier commit Commit e3df29d488f5ce50ee39 introduced this bug where the interfaces-were-changed-or-not bit got lost. Signed-off-by: Brad Fitzpatrick --- wgengine/userspace.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 076eefa70..d61b9645d 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -250,7 +250,7 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ unregisterMonWatch := e.linkMon.RegisterChangeCallback(func(changed bool, st *interfaces.State) { tshttpproxy.InvalidateCache() - e.linkChange(false, st) + e.linkChange(changed, st) }) closePool.addFunc(unregisterMonWatch) e.linkMonUnregister = unregisterMonWatch From 625c413508c031a25e829b1c4d9acb14db4d24fe Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 20:45:30 -0800 Subject: [PATCH 14/48] ipn/ipnlocal: fix another regression from link monitoring refactor Prior to e3df29d488f5ce50ee39, the Engine.SetLinkChangeCallback fired immediately, even if there was no change. The ipnlocal code apparently depended on that, and it broke integration tests (which live in another repo). So mimic the old behavior and call the ipnlocal callback immediately at init. Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3fd31375b..e3a60515a 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -139,14 +139,19 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge portpoll: portpoll, gotPortPollRes: make(chan struct{}), } - b.unregisterLinkMon = e.GetLinkMonitor().RegisterChangeCallback(b.linkChange) b.statusChanged = sync.NewCond(&b.statusLock) + linkMon := e.GetLinkMonitor() + // Call our linkChange code once with the current state, and + // then also whenever it changes: + b.linkChange(false, linkMon.InterfaceState()) + b.unregisterLinkMon = linkMon.RegisterChangeCallback(b.linkChange) + return b, nil } -// linkChange is called (in a new goroutine) by wgengine when its link monitor -// detects a network change. +// linkChange is our link monitor callback, called whenever the network changes. +// major is whether ifst is different than earlier. func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) { b.mu.Lock() defer b.mu.Unlock() From 24fa616e7330229579b89d37254ac050bc7d6069 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 21:34:41 -0800 Subject: [PATCH 15/48] wgengine/monitor: make Darwin monitor shut down cleanly, add test Don't use os.NewFile or (*os.File).Close on the AF_ROUTE socket. It apparently does weird things to the fd and at least doesn't seem to close it. Just use the unix package. The test doesn't actually fail reliably before the fix, though. It was an attempt. But this fixes the integration tests. Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor_darwin.go | 19 ++++++++++++------- wgengine/monitor/monitor_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 wgengine/monitor/monitor_test.go diff --git a/wgengine/monitor/monitor_darwin.go b/wgengine/monitor/monitor_darwin.go index 1d7a106a4..2ae9c4433 100644 --- a/wgengine/monitor/monitor_darwin.go +++ b/wgengine/monitor/monitor_darwin.go @@ -7,7 +7,7 @@ package monitor import ( "fmt" "log" - "os" + "sync" "golang.org/x/net/route" "golang.org/x/sys/unix" @@ -31,22 +31,27 @@ func newOSMon(logf logger.Logf) (osMon, error) { } return &darwinRouteMon{ logf: logf, - f: os.NewFile(uintptr(fd), "AF_ROUTE"), + fd: fd, }, nil } type darwinRouteMon struct { - logf logger.Logf - f *os.File // AF_ROUTE socket - buf [2 << 10]byte + logf logger.Logf + fd int // AF_ROUTE socket + buf [2 << 10]byte + closeOnce sync.Once } func (m *darwinRouteMon) Close() error { - return m.f.Close() + var err error + m.closeOnce.Do(func() { + err = unix.Close(m.fd) + }) + return err } func (m *darwinRouteMon) Receive() (message, error) { - n, err := m.f.Read(m.buf[:]) + n, err := unix.Read(m.fd, m.buf[:]) if err != nil { return nil, err } diff --git a/wgengine/monitor/monitor_test.go b/wgengine/monitor/monitor_test.go new file mode 100644 index 000000000..9c9019eeb --- /dev/null +++ b/wgengine/monitor/monitor_test.go @@ -0,0 +1,30 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package monitor + +import ( + "testing" +) + +func TestMonitorStartClose(t *testing.T) { + mon, err := New(t.Logf) + if err != nil { + t.Fatal(err) + } + mon.Start() + if err := mon.Close(); err != nil { + t.Fatal(err) + } +} + +func TestMonitorJustClose(t *testing.T) { + mon, err := New(t.Logf) + if err != nil { + t.Fatal(err) + } + if err := mon.Close(); err != nil { + t.Fatal(err) + } +} From c0cdca6d0668ac22911c9db12b0524d5efd50a4e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Mar 2021 22:09:43 -0800 Subject: [PATCH 16/48] cmd/tailscaled, logtail: share link monitor from wgengine to logtail Part of overall effort to clean up, unify, use link monitoring more, and make Tailscale quieter when all networks are down. This is especially bad on macOS where we can get killed for not being polite it seems. (But we should be polite in any case) Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/tailscaled.go | 10 +++++++++- logtail/logtail.go | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index d96bff392..f4a5f9b06 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -36,6 +36,7 @@ import ( "tailscale.com/version" "tailscale.com/wgengine" "tailscale.com/wgengine/magicsock" + "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/netstack" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tstun" @@ -195,6 +196,12 @@ func run() error { go runDebugServer(debugMux, args.debug) } + linkMon, err := monitor.New(logf) + if err != nil { + log.Fatalf("creating link monitor: %v", err) + } + pol.Logtail.SetLinkMonitor(linkMon) + var socksListener net.Listener if args.socksAddr != "" { var err error @@ -205,7 +212,8 @@ func run() error { } conf := wgengine.Config{ - ListenPort: args.port, + ListenPort: args.port, + LinkMonitor: linkMon, } if args.tunname == "userspace-networking" { conf.TUN = tstun.NewFakeTUN() diff --git a/logtail/logtail.go b/logtail/logtail.go index bad60d011..e0e9f93be 100644 --- a/logtail/logtail.go +++ b/logtail/logtail.go @@ -19,6 +19,7 @@ import ( "tailscale.com/logtail/backoff" tslogger "tailscale.com/types/logger" + "tailscale.com/wgengine/monitor" ) // DefaultHost is the default host name to upload logs to when @@ -106,6 +107,7 @@ type Logger struct { url string lowMem bool skipClientTime bool + linkMonitor *monitor.Mon buffer Buffer sent chan struct{} // signal to speed up drain drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain @@ -128,6 +130,14 @@ func (l *Logger) SetVerbosityLevel(level int) { l.stderrLevel = level } +// SetLinkMonitor sets the optional the link monitor. +// +// It should not be changed concurrently with log writes and should +// only be set once. +func (l *Logger) SetLinkMonitor(lm *monitor.Mon) { + l.linkMonitor = lm +} + // Shutdown gracefully shuts down the logger while completing any // remaining uploads. // From b89c757817413d75edb2e687bddde8e94a76a956 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Fri, 26 Feb 2021 11:16:12 -0500 Subject: [PATCH 17/48] wgengine/tsdns: explicitly reject .onion lookups Tor has a location-hidden service feature that enables users to host services from inside the Tor network. Each of these gets a unique DNS name that ends with .onion. As it stands now, if a misbehaving application somehow manages to make a .onion DNS request to our DNS server, we will forward that to the DNS server, which could leak that to malicious third parties. See the recent bug Brave had with this[1] for more context. RFC 7686 suggests that name resolution APIs and libraries MUST respond with NXDOMAIN unless they can actually handle Tor lookups. We can't handle .onion lookups, so we reject them. [1]: https://twitter.com/albinowax/status/1362737949872431108 Fixes tailscale/corp#1351 Signed-off-by: Christine Dodrill --- wgengine/tsdns/tsdns.go | 5 +++++ wgengine/tsdns/tsdns_test.go | 1 + 2 files changed, 6 insertions(+) diff --git a/wgengine/tsdns/tsdns.go b/wgengine/tsdns/tsdns.go index 680bf51b3..23db7bba4 100644 --- a/wgengine/tsdns/tsdns.go +++ b/wgengine/tsdns/tsdns.go @@ -194,6 +194,11 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet } + // Reject .onion domains per RFC 7686. + if dnsname.HasSuffix(domain, ".onion") { + return netaddr.IP{}, dns.RCodeNameError, nil + } + anyHasSuffix := false for _, suffix := range dnsMap.rootDomains { if dnsname.HasSuffix(domain, suffix) { diff --git a/wgengine/tsdns/tsdns_test.go b/wgengine/tsdns/tsdns_test.go index a2f56a168..66a62d107 100644 --- a/wgengine/tsdns/tsdns_test.go +++ b/wgengine/tsdns/tsdns_test.go @@ -219,6 +219,7 @@ func TestResolve(t *testing.T) { {"mx-ipv6", "test2.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeSuccess}, {"mx-nxdomain", "test3.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeNameError}, {"ns-nxdomain", "test3.ipn.dev.", dns.TypeNS, netaddr.IP{}, dns.RCodeNameError}, + {"onion-domain", "footest.onion.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError}, } for _, tt := range tests { From 31721759f3d101d9df821be12310451573182c1d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 08:15:57 -0800 Subject: [PATCH 18/48] wgengine/monitor: don't return nil, nil in darwin monitor We used to allow that, but now it just crashes. Separately I need to figure out why it got into this path at all, which is #1416. Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor_darwin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgengine/monitor/monitor_darwin.go b/wgengine/monitor/monitor_darwin.go index 2ae9c4433..a2397617d 100644 --- a/wgengine/monitor/monitor_darwin.go +++ b/wgengine/monitor/monitor_darwin.go @@ -58,7 +58,7 @@ func (m *darwinRouteMon) Receive() (message, error) { msgs, err := route.ParseRIB(route.RIBTypeRoute, m.buf[:n]) if err != nil { m.logf("read %d bytes (% 02x), failed to parse RIB: %v", n, m.buf[:n], err) - return nil, nil + return unspecifiedMessage{}, nil } if debugRouteMessages { m.logf("read: %d bytes, %d msgs", n, len(msgs)) From 0d0ec7853cccc308bfb548082763418f8b231704 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 08:36:25 -0800 Subject: [PATCH 19/48] cmd/tailscaled: don't require root on darwin with --tun=userspace-networking Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/tailscaled.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index f4a5f9b06..59204b16e 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -137,9 +137,9 @@ func main() { os.Exit(0) } - if runtime.GOOS == "darwin" && os.Getuid() != 0 { + if runtime.GOOS == "darwin" && os.Getuid() != 0 && useTUN() { log.SetFlags(0) - log.Fatalf("tailscaled requires root; use sudo tailscaled") + log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)") } if args.socketpath == "" && runtime.GOOS != "windows" { @@ -215,11 +215,11 @@ func run() error { ListenPort: args.port, LinkMonitor: linkMon, } - if args.tunname == "userspace-networking" { + if useTUN() { + conf.TUNName = args.tunname + } else { conf.TUN = tstun.NewFakeTUN() conf.RouterGen = router.NewFake - } else { - conf.TUNName = args.tunname } e, err := wgengine.NewUserspaceEngine(logf, conf) @@ -229,7 +229,7 @@ func run() error { } var ns *netstack.Impl - if args.tunname == "userspace-networking" { + if useNetstack() { tunDev, magicConn := e.(wgengine.InternalsGetter).GetInternals() ns, err = netstack.Create(logf, tunDev, e, magicConn) if err != nil { @@ -244,7 +244,7 @@ func run() error { srv := &socks5.Server{ Logf: logger.WithPrefix(logf, "socks5: "), } - if args.tunname == "userspace-networking" { + if useNetstack() { srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { return ns.DialContextTCP(ctx, addr) } @@ -329,3 +329,6 @@ func runDebugServer(mux *http.ServeMux, addr string) { log.Fatal(err) } } + +func useTUN() bool { return args.tunname != "userspace-networking" } +func useNetstack() bool { return !useTUN() } From f304a45481b3721527b352a4648c2ecad941b9ff Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 09:53:36 -0800 Subject: [PATCH 20/48] wgengine/monitor: add skipped failing test for Darwin route message bug Updates #1416 Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor_darwin_test.go | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 wgengine/monitor/monitor_darwin_test.go diff --git a/wgengine/monitor/monitor_darwin_test.go b/wgengine/monitor/monitor_darwin_test.go new file mode 100644 index 000000000..48456b93e --- /dev/null +++ b/wgengine/monitor/monitor_darwin_test.go @@ -0,0 +1,28 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package monitor + +import ( + "encoding/hex" + "strings" + "testing" + + "golang.org/x/net/route" +) + +func TestIssue1416RIB(t *testing.T) { + const ribHex = `32 00 05 10 30 00 00 00 00 00 00 00 04 00 00 00 14 12 04 00 06 03 06 00 65 6e 30 ac 87 a3 19 7f 82 00 00 00 0e 12 00 00 00 00 06 00 91 e0 f0 01 00 00` + rtmMsg, err := hex.DecodeString(strings.ReplaceAll(ribHex, " ", "")) + if err != nil { + t.Fatal(err) + } + msgs, err := route.ParseRIB(route.RIBTypeRoute, rtmMsg) + if err != nil { + t.Logf("ParseRIB: %v", err) + t.Skip("skipping on known failure; see https://github.com/tailscale/tailscale/issues/1416") + t.Fatal(err) + } + t.Logf("Got: %#v", msgs) +} From be779b3587a79da60a27e1bcd18eec6528354221 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 11:12:14 -0800 Subject: [PATCH 21/48] safesocket, ipn/ipnserver: unify peercred info, fix bug on FreeBSD etc FreeBSD wasn't able to run "tailscale up" since the recent peercred refactoring. Signed-off-by: Brad Fitzpatrick --- ipn/ipnserver/server.go | 3 +++ safesocket/safesocket.go | 10 ++++++++++ safesocket/unixsocket.go | 16 +--------------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index b37fee0ee..15bbba694 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -319,6 +319,9 @@ func isReadonlyConn(c net.Conn, logf logger.Logf) bool { } const ro = true const rw = false + if !safesocket.PlatformUsesPeerCreds() { + return rw + } creds, err := peercred.Get(c) if err != nil { logf("connection from unknown peer; read-only") diff --git a/safesocket/safesocket.go b/safesocket/safesocket.go index 5cbf73770..e27cdb302 100644 --- a/safesocket/safesocket.go +++ b/safesocket/safesocket.go @@ -65,3 +65,13 @@ func LocalTCPPortAndToken() (port int, token string, err error) { } return localTCPPortAndToken() } + +// PlatformUsesPeerCreds reports whether the current platform uses peer credentials +// to authenticate connections. +func PlatformUsesPeerCreds() bool { + switch runtime.GOOS { + case "linux", "darwin": + return true + } + return false +} diff --git a/safesocket/unixsocket.go b/safesocket/unixsocket.go index f86d55367..303b9c27d 100644 --- a/safesocket/unixsocket.go +++ b/safesocket/unixsocket.go @@ -103,21 +103,7 @@ func tailscaledRunningUnderLaunchd() bool { // socketPermissionsForOS returns the permissions to use for the // tailscaled.sock. func socketPermissionsForOS() os.FileMode { - switch runtime.GOOS { - case "linux", "darwin": - // On Linux and Darwin, the ipn/ipnserver package looks at the Unix peer creds - // and only permits read-only actions from non-root users, so we want - // this opened up wider. - // - // TODO(bradfitz): unify this all one in place probably, moving some - // of ipnserver (which does much of the "safe" bits) here. Maybe - // instead of net.Listener, we should return a type that returns - // an identity in addition to a net.Conn? (returning a wrapped net.Conn - // would surprise downstream callers probably) - // - // TODO(bradfitz): if OpenBSD and FreeBSD do the equivalent peercreds - // stuff that's in ipn/ipnserver/conn_ucred.go, they should also - // return 0666 here. + if PlatformUsesPeerCreds() { return 0666 } // Otherwise, root only. From 471f0c470a6fe5375940b20147fef723ef91c965 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 10:29:49 -0800 Subject: [PATCH 22/48] wgengine/monitor: skip some macOS route updates, fix debounce regression Debound was broken way back in 5c1e443d348d32 and we never noticed. Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor.go | 23 +++++++++----- wgengine/monitor/monitor_darwin.go | 49 +++++++++++++++++++++++------- wgengine/monitor/monitor_test.go | 35 +++++++++++++++++++++ 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 355da7da9..75441b088 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -143,17 +143,24 @@ func (m *Mon) Close() error { return err } +func (m *Mon) stopped() bool { + select { + case <-m.stop: + return true + default: + return false + } +} + // pump continuously retrieves messages from the connection, notifying // the change channel of changes, and stopping when a stop is issued. func (m *Mon) pump() { defer m.goroutines.Done() - for { + for !m.stopped() { msg, err := m.om.Receive() if err != nil { - select { - case <-m.stop: + if m.stopped() { return - default: } // Keep retrying while we're not closed. m.logf("error from link monitor: %v", err) @@ -165,8 +172,10 @@ func (m *Mon) pump() { } select { case m.change <- struct{}{}: - case <-m.stop: - return + default: + // Another change signal is already + // buffered. Debounce will wake up soon + // enough. } } } @@ -199,7 +208,7 @@ func (m *Mon) debounce() { select { case <-m.stop: return - case <-time.After(100 * time.Millisecond): + case <-time.After(250 * time.Millisecond): } } } diff --git a/wgengine/monitor/monitor_darwin.go b/wgengine/monitor/monitor_darwin.go index a2397617d..8385d1342 100644 --- a/wgengine/monitor/monitor_darwin.go +++ b/wgengine/monitor/monitor_darwin.go @@ -51,20 +51,47 @@ func (m *darwinRouteMon) Close() error { } func (m *darwinRouteMon) Receive() (message, error) { - n, err := unix.Read(m.fd, m.buf[:]) - if err != nil { - return nil, err - } - msgs, err := route.ParseRIB(route.RIBTypeRoute, m.buf[:n]) - if err != nil { - m.logf("read %d bytes (% 02x), failed to parse RIB: %v", n, m.buf[:n], err) + for { + n, err := unix.Read(m.fd, m.buf[:]) + if err != nil { + return nil, err + } + msgs, err := route.ParseRIB(route.RIBTypeRoute, m.buf[:n]) + if err != nil { + if debugRouteMessages { + m.logf("read %d bytes (% 02x), failed to parse RIB: %v", n, m.buf[:n], err) + } + return unspecifiedMessage{}, nil + } + if len(msgs) == 0 { + if debugRouteMessages { + m.logf("read %d bytes with no messages (% 02x)", n, m.buf[:n]) + } + continue + } + nSkip := 0 + for _, msg := range msgs { + if m.skipMessage(msg) { + nSkip++ + } + } + if debugRouteMessages { + m.logf("read %d bytes, %d messages (%d skipped)", n, len(msgs), nSkip) + m.logMessages(msgs) + } + if nSkip == len(msgs) { + continue + } return unspecifiedMessage{}, nil } - if debugRouteMessages { - m.logf("read: %d bytes, %d msgs", n, len(msgs)) - m.logMessages(msgs) +} + +func (m *darwinRouteMon) skipMessage(msg route.Message) bool { + switch msg.(type) { + case *route.InterfaceMulticastAddrMessage: + return true } - return unspecifiedMessage{}, nil + return false } func (m *darwinRouteMon) logMessages(msgs []route.Message) { diff --git a/wgengine/monitor/monitor_test.go b/wgengine/monitor/monitor_test.go index 9c9019eeb..f1e35439e 100644 --- a/wgengine/monitor/monitor_test.go +++ b/wgengine/monitor/monitor_test.go @@ -5,7 +5,10 @@ package monitor import ( + "flag" "testing" + + "tailscale.com/net/interfaces" ) func TestMonitorStartClose(t *testing.T) { @@ -28,3 +31,35 @@ func TestMonitorJustClose(t *testing.T) { t.Fatal(err) } } + +var monitor = flag.String("monitor", "", `go into monitor mode like 'route monitor'; test never terminates. Value can be either "raw" or "callback"`) + +func TestMonitorMode(t *testing.T) { + switch *monitor { + case "": + t.Skip("skipping non-test without --monitor") + case "raw", "callback": + default: + t.Skipf(`invalid --monitor value: must be "raw" or "callback"`) + } + mon, err := New(t.Logf) + if err != nil { + t.Fatal(err) + } + switch *monitor { + case "raw": + for { + msg, err := mon.om.Receive() + if err != nil { + t.Fatal(err) + } + t.Logf("msg: %#v", msg) + } + case "callback": + mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) { + t.Logf("cb: changed=%v, ifSt=%v", changed, st) + }) + mon.Start() + select {} + } +} From 95c03d1ead8a53fba64941ea3acfeb65b6f22ea0 Mon Sep 17 00:00:00 2001 From: Naman Sood Date: Tue, 2 Mar 2021 15:14:29 -0500 Subject: [PATCH 23/48] wgengine/netstack: forward incoming connections to localhost Updates #707 Updates #504 Signed-off-by: Naman Sood --- wgengine/netstack/netstack.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 5bf93a8b5..2332dc167 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -322,19 +322,18 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) { return } localAddr, err := ep.GetLocalAddress() - ns.logf("[v2] forwarding port %v to 100.101.102.103:80", localAddr.Port) if err != nil { r.Complete(true) return } r.Complete(false) c := gonet.NewTCPConn(&wq, ep) - go ns.forwardTCP(c, &wq, "100.101.102.103:80") + go ns.forwardTCP(c, &wq, localAddr.Port) } -func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, address string) { +func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, port uint16) { defer client.Close() - ns.logf("[v2] netstack: forwarding to address %s", address) + ns.logf("[v2] netstack: forwarding incoming connection on port %v", port) ctx, cancel := context.WithCancel(context.Background()) defer cancel() waitEntry, notifyCh := waiter.NewChannelEntry(nil) @@ -351,23 +350,26 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, address stri } cancel() }() - server, err := ns.DialContextTCP(ctx, address) + server, err := ns.DialContextTCP(ctx, net.JoinHostPort("localhost", strconv.Itoa(int(port)))) if err != nil { - ns.logf("netstack: could not connect to server %s: %s", address, err) + ns.logf("netstack: could not connect to local server on port %v: %v", port, err) return } defer server.Close() - connClosed := make(chan bool, 2) + connClosed := make(chan error, 2) go func() { - io.Copy(server, client) - connClosed <- true + _, err := io.Copy(server, client) + connClosed <- err }() go func() { - io.Copy(client, server) - connClosed <- true + _, err := io.Copy(client, server) + connClosed <- err }() - <-connClosed - ns.logf("[v2] netstack: forwarder connection to %s closed", address) + err = <-connClosed + if err != nil { + ns.logf("proxy connection closed with error: %v", err) + } + ns.logf("[v2] netstack: forwarder connection on port %v closed", port) } func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) { From 63ed4dd6c94e509c8585acca3ad9e08220ff245b Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 12:26:43 -0800 Subject: [PATCH 24/48] net/portmapper: fix typo Signed-off-by: Brad Fitzpatrick --- net/portmapper/portmapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index 3e1ef3b79..e22e8932e 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -222,7 +222,7 @@ func IsNoMappingError(err error) bool { var ( ErrNoPortMappingServices = errors.New("no port mapping services were found") - ErrGatewayNotFound = errors.New("failed to look gateway address") + ErrGatewayNotFound = errors.New("failed to look up gateway address") ) // CreateOrGetMapping either creates a new mapping or returns a cached From 15b6969a951b350929bc8b2438e5b6f7daf8b730 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 11:59:48 -0800 Subject: [PATCH 25/48] ipn/ipnserver: grant client r/w access if peer uid matches tailscaled Signed-off-by: Brad Fitzpatrick --- ipn/ipnserver/server.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 15bbba694..ae053f88f 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -19,6 +19,7 @@ import ( "os/signal" "os/user" "runtime" + "strconv" "strings" "sync" "sync/atomic" @@ -336,6 +337,10 @@ func isReadonlyConn(c net.Conn, logf logger.Logf) bool { logf("connection from userid %v; root has access", uid) return rw } + if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) { + logf("connection from userid %v; connection from non-root user matching daemon has access", uid) + return rw + } var adminGroupID string switch runtime.GOOS { case "darwin": From c3e5903b9149be1d1b7da24f3fe6b8886199dca4 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 12:42:27 -0800 Subject: [PATCH 26/48] wgengine/magicsock: remove leftover portmapper debug logging It's already logged at the right time in logEndpointChange. Signed-off-by: Brad Fitzpatrick --- wgengine/magicsock/magicsock.go | 1 - 1 file changed, 1 deletion(-) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 5af5632df..bcb6f750b 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -1015,7 +1015,6 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason } if ext, err := c.portMapper.CreateOrGetMapping(ctx); err == nil { - c.logf("portmapper: using %v", ext) addAddr(ext.String(), "portmap") } else if !portmapper.IsNoMappingError(err) { c.logf("portmapper: %v", err) From b4cf837d8adc447233bac47e3425d5a3bcda87a5 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 15:21:32 -0800 Subject: [PATCH 27/48] logtail: use link monitor to determine when to retry after upload failure Signed-off-by: Brad Fitzpatrick --- logtail/logtail.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/logtail/logtail.go b/logtail/logtail.go index e0e9f93be..d9ed73532 100644 --- a/logtail/logtail.go +++ b/logtail/logtail.go @@ -18,6 +18,7 @@ import ( "time" "tailscale.com/logtail/backoff" + "tailscale.com/net/interfaces" tslogger "tailscale.com/types/logger" "tailscale.com/wgengine/monitor" ) @@ -287,6 +288,11 @@ func (l *Logger) uploading(ctx context.Context) { } uploaded, err := l.upload(ctx, body, origlen) if err != nil { + if !l.internetUp() { + fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n") + l.awaitInternetUp(ctx) + continue + } fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err) } l.bo.BackOff(ctx, err) @@ -303,6 +309,34 @@ func (l *Logger) uploading(ctx context.Context) { } } +func (l *Logger) internetUp() bool { + if l.linkMonitor == nil { + // No way to tell, so assume it is. + return true + } + return l.linkMonitor.InterfaceState().AnyInterfaceUp() +} + +func (l *Logger) awaitInternetUp(ctx context.Context) { + upc := make(chan bool, 1) + defer l.linkMonitor.RegisterChangeCallback(func(changed bool, st *interfaces.State) { + if st.AnyInterfaceUp() { + select { + case upc <- true: + default: + } + } + })() + if l.internetUp() { + return + } + select { + case <-upc: + fmt.Fprintf(l.stderr, "logtail: internet back up\n") + case <-ctx.Done(): + } +} + // upload uploads body to the log server. // origlen indicates the pre-compression body length. // origlen of -1 indicates that the body is not compressed. From 8d77dfdacbba75f63a518501a5da4964c6166116 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 2 Mar 2021 19:32:04 -0800 Subject: [PATCH 28/48] wgengine/router: add a dummy IPv6 address if needed for default routing. Fixes #1339 Signed-off-by: David Anderson --- net/tsaddr/tsaddr.go | 11 +++++++++++ wgengine/router/ifconfig_windows.go | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go index 76dd65f82..44cf5cf23 100644 --- a/net/tsaddr/tsaddr.go +++ b/net/tsaddr/tsaddr.go @@ -71,6 +71,17 @@ func Tailscale4To6Range() netaddr.IPPrefix { return ula4To6Range.v } +// Tailscale4To6Placeholder returns an IP address that can be used as +// a source IP when one is required, but a netmap didn't provide +// any. This address never gets allocated by the 4-to-6 algorithm in +// control. +// +// Currently used to work around a Windows limitation when programming +// IPv6 routes in corner cases. +func Tailscale4To6Placeholder() netaddr.IP { + return Tailscale4To6Range().IP +} + // Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the // given Tailscale IPv4 address. Returns a zero IP if ipv4 isn't a // Tailscale IPv4 address. diff --git a/wgengine/router/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go index 9e7f4be3b..4a331319f 100644 --- a/wgengine/router/ifconfig_windows.go +++ b/wgengine/router/ifconfig_windows.go @@ -21,6 +21,7 @@ import ( "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/net/interfaces" + "tailscale.com/net/tsaddr" "tailscale.com/wgengine/winnet" ) @@ -305,7 +306,17 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { foundDefault4 := false foundDefault6 := false for _, route := range cfg.Routes { - if (route.IP.Is4() && firstGateway4 == nil) || (route.IP.Is6() && firstGateway6 == nil) { + if route.IP.Is6() && firstGateway6 == nil { + // Windows won't let us set IPv6 routes without having an + // IPv6 local address set. However, when we've configured + // a default route, we want to forcibly grab IPv6 traffic + // even if the v6 overlay network isn't configured. To do + // that, we add a dummy local IPv6 address to serve as a + // route source. + ipnet := &net.IPNet{tsaddr.Tailscale4To6Placeholder().IPAddr().IP, net.CIDRMask(128, 128)} + addresses = append(addresses, ipnet) + firstGateway6 = &ipnet.IP + } else if route.IP.Is4() && firstGateway4 == nil { return errors.New("Due to a Windows limitation, one cannot have interface routes without an interface address") } From 8a55d463c810e6d39b1179d09d1347e52c9167fc Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 21:13:29 -0800 Subject: [PATCH 29/48] net/interfaces: merge darwin files for DefaultRouteInterface in sandbox DefaultRouteInterface was previously guarded by build tags such that it was only accessible to tailscaled-on-macos, but there was no reason for that. It runs fine in the sandbox and gives better default info, so merge its file into interfaces_darwin.go. Signed-off-by: Brad Fitzpatrick --- net/interfaces/interfaces_darwin.go | 66 +++++++++++++++ .../interfaces_darwin_tailscaled.go | 81 ------------------- .../interfaces_defaultrouteif_todo.go | 2 +- 3 files changed, 67 insertions(+), 82 deletions(-) delete mode 100644 net/interfaces/interfaces_darwin_tailscaled.go diff --git a/net/interfaces/interfaces_darwin.go b/net/interfaces/interfaces_darwin.go index c502255a9..9d0f0526f 100644 --- a/net/interfaces/interfaces_darwin.go +++ b/net/interfaces/interfaces_darwin.go @@ -6,9 +6,13 @@ package interfaces import ( "errors" + "fmt" + "net" "os/exec" + "syscall" "go4.org/mem" + "golang.org/x/net/route" "inet.af/netaddr" "tailscale.com/util/lineread" "tailscale.com/version" @@ -72,3 +76,65 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) { } var errStopReadingNetstatTable = errors.New("found private gateway") + +func DefaultRouteInterface() (string, error) { + idx, err := DefaultRouteInterfaceIndex() + if err != nil { + return "", err + } + iface, err := net.InterfaceByIndex(idx) + if err != nil { + return "", err + } + return iface.Name, nil +} + +func DefaultRouteInterfaceIndex() (int, error) { + // $ netstat -nr + // Routing tables + // Internet: + // Destination Gateway Flags Netif Expire + // default 10.0.0.1 UGSc en0 <-- want this one + // default 10.0.0.1 UGScI en1 + + // From man netstat: + // U RTF_UP Route usable + // G RTF_GATEWAY Destination requires forwarding by intermediary + // S RTF_STATIC Manually added + // c RTF_PRCLONING Protocol-specified generate new routes on use + // I RTF_IFSCOPE Route is associated with an interface scope + + rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0) + if err != nil { + return 0, fmt.Errorf("route.FetchRIB: %w", err) + } + msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib) + if err != nil { + return 0, fmt.Errorf("route.ParseRIB: %w", err) + } + indexSeen := map[int]int{} // index => count + for _, m := range msgs { + rm, ok := m.(*route.RouteMessage) + if !ok { + continue + } + const RTF_GATEWAY = 0x2 + const RTF_IFSCOPE = 0x1000000 + if rm.Flags&RTF_GATEWAY == 0 { + continue + } + if rm.Flags&RTF_IFSCOPE != 0 { + continue + } + indexSeen[rm.Index]++ + } + if len(indexSeen) == 0 { + return 0, errors.New("no gateway index found") + } + if len(indexSeen) == 1 { + for idx := range indexSeen { + return idx, nil + } + } + return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen) +} diff --git a/net/interfaces/interfaces_darwin_tailscaled.go b/net/interfaces/interfaces_darwin_tailscaled.go deleted file mode 100644 index 1dd598619..000000000 --- a/net/interfaces/interfaces_darwin_tailscaled.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build darwin,!redo,!ios -// (Exclude redo, because we don't want this code in the App Store -// version's sandbox, where it won't work, and also don't want it on -// iOS. This is just for utun-using non-sandboxed cmd/tailscaled on macOS. - -package interfaces - -import ( - "errors" - "fmt" - "net" - "syscall" - - "golang.org/x/net/route" -) - -func DefaultRouteInterface() (string, error) { - idx, err := DefaultRouteInterfaceIndex() - if err != nil { - return "", err - } - iface, err := net.InterfaceByIndex(idx) - if err != nil { - return "", err - } - return iface.Name, nil -} - -func DefaultRouteInterfaceIndex() (int, error) { - // $ netstat -nr - // Routing tables - // Internet: - // Destination Gateway Flags Netif Expire - // default 10.0.0.1 UGSc en0 <-- want this one - // default 10.0.0.1 UGScI en1 - - // From man netstat: - // U RTF_UP Route usable - // G RTF_GATEWAY Destination requires forwarding by intermediary - // S RTF_STATIC Manually added - // c RTF_PRCLONING Protocol-specified generate new routes on use - // I RTF_IFSCOPE Route is associated with an interface scope - - rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0) - if err != nil { - return 0, fmt.Errorf("route.FetchRIB: %w", err) - } - msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib) - if err != nil { - return 0, fmt.Errorf("route.ParseRIB: %w", err) - } - indexSeen := map[int]int{} // index => count - for _, m := range msgs { - rm, ok := m.(*route.RouteMessage) - if !ok { - continue - } - const RTF_GATEWAY = 0x2 - const RTF_IFSCOPE = 0x1000000 - if rm.Flags&RTF_GATEWAY == 0 { - continue - } - if rm.Flags&RTF_IFSCOPE != 0 { - continue - } - indexSeen[rm.Index]++ - } - if len(indexSeen) == 0 { - return 0, errors.New("no gateway index found") - } - if len(indexSeen) == 1 { - for idx := range indexSeen { - return idx, nil - } - } - return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen) -} diff --git a/net/interfaces/interfaces_defaultrouteif_todo.go b/net/interfaces/interfaces_defaultrouteif_todo.go index 255543336..7e1c33af4 100644 --- a/net/interfaces/interfaces_defaultrouteif_todo.go +++ b/net/interfaces/interfaces_defaultrouteif_todo.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !linux,!windows,!darwin darwin,redo +// +build !linux,!windows,!darwin package interfaces From 7461dded88b86e93e45a2fa375fa652f2519e211 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 21:45:40 -0800 Subject: [PATCH 30/48] wgengine/monitor: on unsupported platforms, use a polling implementation Not great, but lets people working on new ports get going more quickly without having to do everything up front. As the link monitor is getting used more, I felt bad having a useless implementation. Updates #815 Updates #1427 Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor.go | 15 ++++-- wgengine/monitor/monitor_darwin.go | 2 +- wgengine/monitor/monitor_freebsd.go | 2 +- wgengine/monitor/monitor_linux.go | 2 +- wgengine/monitor/monitor_polling.go | 72 +++++++++++++++++++++++++ wgengine/monitor/monitor_unsupported.go | 11 ---- wgengine/monitor/monitor_windows.go | 2 +- 7 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 wgengine/monitor/monitor_polling.go delete mode 100644 wgengine/monitor/monitor_unsupported.go diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 75441b088..9f8610a06 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -8,6 +8,7 @@ package monitor import ( + "errors" "sync" "time" @@ -63,14 +64,9 @@ type Mon struct { // Use RegisterChangeCallback to get notified of network changes. func New(logf logger.Logf) (*Mon, error) { logf = logger.WithPrefix(logf, "monitor: ") - om, err := newOSMon(logf) - if err != nil { - return nil, err - } m := &Mon{ logf: logf, cbs: map[*callbackHandle]ChangeFunc{}, - om: om, change: make(chan struct{}, 1), stop: make(chan struct{}), } @@ -79,6 +75,15 @@ func New(logf logger.Logf) (*Mon, error) { return nil, err } m.ifState = st + + m.om, err = newOSMon(logf, m) + if err != nil { + return nil, err + } + if m.om == nil { + return nil, errors.New("newOSMon returned nil, nil") + } + return m, nil } diff --git a/wgengine/monitor/monitor_darwin.go b/wgengine/monitor/monitor_darwin.go index 8385d1342..1bf3a4d20 100644 --- a/wgengine/monitor/monitor_darwin.go +++ b/wgengine/monitor/monitor_darwin.go @@ -24,7 +24,7 @@ type unspecifiedMessage struct{} func (unspecifiedMessage) ignore() bool { return false } -func newOSMon(logf logger.Logf) (osMon, error) { +func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) { fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) if err != nil { return nil, err diff --git a/wgengine/monitor/monitor_freebsd.go b/wgengine/monitor/monitor_freebsd.go index 2d324c454..1230d56ff 100644 --- a/wgengine/monitor/monitor_freebsd.go +++ b/wgengine/monitor/monitor_freebsd.go @@ -25,7 +25,7 @@ type devdConn struct { conn net.Conn } -func newOSMon(logf logger.Logf) (osMon, error) { +func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) { conn, err := net.Dial("unixpacket", "/var/run/devd.seqpacket.pipe") if err != nil { return nil, fmt.Errorf("devd dial error: %v", err) diff --git a/wgengine/monitor/monitor_linux.go b/wgengine/monitor/monitor_linux.go index aa7cded06..ef867bae3 100644 --- a/wgengine/monitor/monitor_linux.go +++ b/wgengine/monitor/monitor_linux.go @@ -37,7 +37,7 @@ type nlConn struct { buffered []netlink.Message } -func newOSMon(logf logger.Logf) (osMon, error) { +func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) { conn, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ // Routes get us most of the events of interest, but we need // address as well to cover things like DHCP deciding to give diff --git a/wgengine/monitor/monitor_polling.go b/wgengine/monitor/monitor_polling.go new file mode 100644 index 000000000..079c956bb --- /dev/null +++ b/wgengine/monitor/monitor_polling.go @@ -0,0 +1,72 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !linux,!freebsd,!windows,!darwin android + +package monitor + +import ( + "errors" + "runtime" + "sync" + "time" + + "tailscale.com/types/logger" +) + +func newOSMon(logf logger.Logf, m *Mon) (osMon, error) { + return &pollingMon{ + logf: logf, + m: m, + stop: make(chan struct{}), + }, nil +} + +// pollingMon is a bad but portable implementation of the link monitor +// that works by polling the interface state every 10 seconds, in lieu +// of anything to subscribe to. A good implementation +type pollingMon struct { + logf logger.Logf + m *Mon + + closeOnce sync.Once + stop chan struct{} +} + +func (pm *pollingMon) Close() error { + pm.closeOnce.Do(func() { + close(pm.stop) + }) + return nil +} + +func (pm *pollingMon) Receive() (message, error) { + d := 10 * time.Second + if runtime.GOOS == "android" { + // We'll have Android notify the link monitor to wake up earlier, + // so this can go very slowly there, to save battery. + // https://github.com/tailscale/tailscale/issues/1427 + d = 10 * time.Minute + } + ticker := time.NewTicker(d) + defer ticker.Stop() + base := pm.m.InterfaceState() + for { + if cur, err := pm.m.interfaceStateUncached(); err == nil && !cur.Equal(base) { + return unspecifiedMessage{}, nil + } + select { + case <-ticker.C: + case <-pm.stop: + return nil, errors.New("stopped") + } + } +} + +// unspecifiedMessage is a minimal message implementation that should not +// be ignored. In general, OS-specific implementations should use better +// types and avoid this if they can. +type unspecifiedMessage struct{} + +func (unspecifiedMessage) ignore() bool { return false } diff --git a/wgengine/monitor/monitor_unsupported.go b/wgengine/monitor/monitor_unsupported.go deleted file mode 100644 index 4b7138d6b..000000000 --- a/wgengine/monitor/monitor_unsupported.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !linux,!freebsd,!windows,!darwin android - -package monitor - -import "tailscale.com/types/logger" - -func newOSMon(logger.Logf) (osMon, error) { return nil, nil } diff --git a/wgengine/monitor/monitor_windows.go b/wgengine/monitor/monitor_windows.go index b8297d2e8..3bb39cb74 100644 --- a/wgengine/monitor/monitor_windows.go +++ b/wgengine/monitor/monitor_windows.go @@ -65,7 +65,7 @@ type winMon struct { inFastPoll bool // recent net change event made us go into fast polling mode (to detect proxy changes) } -func newOSMon(logf logger.Logf) (osMon, error) { +func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) { closeHandle, err := windows.CreateEvent(nil, 1 /* manual reset */, 0 /* unsignaled */, nil /* no name */) if err != nil { return nil, fmt.Errorf("CreateEvent: %w", err) From d01c60dad5e71145c42b62a508801c35b67c821d Mon Sep 17 00:00:00 2001 From: Naman Sood Date: Wed, 3 Mar 2021 11:17:14 -0500 Subject: [PATCH 31/48] wgengine/netstack: use system dialer to contact servers on localhost Updates #504 Updates #707 Signed-off-by: Naman Sood --- wgengine/netstack/netstack.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 2332dc167..45724d27c 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -350,7 +350,8 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, port uint16) } cancel() }() - server, err := ns.DialContextTCP(ctx, net.JoinHostPort("localhost", strconv.Itoa(int(port)))) + var stdDialer net.Dialer + server, err := stdDialer.DialContext(ctx, "tcp", net.JoinHostPort("localhost", strconv.Itoa(int(port)))) if err != nil { ns.logf("netstack: could not connect to local server on port %v: %v", port, err) return From f858b0d25f078a5849aedb643651fd3b2c4e0426 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 3 Mar 2021 10:37:01 -0800 Subject: [PATCH 32/48] wgengine/netstack: remove some v2 logging by default Even with [v2], it still logtails and takes time to format. Signed-off-by: Brad Fitzpatrick --- wgengine/netstack/netstack.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 45724d27c..df03a6bae 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -42,6 +42,8 @@ import ( "tailscale.com/wgengine/tstun" ) +const debugNetstack = false + // Impl contains the state for the netstack implementation, // and implements wgengine.FakeImpl to act as a userspace network // stack when Tailscale is running in fake mode. @@ -286,8 +288,9 @@ func (ns *Impl) injectOutbound() { full = append(full, hdrNetwork.View()...) full = append(full, hdrTransport.View()...) full = append(full, pkt.Data.ToView()...) - - ns.logf("[v2] packet Write out: % x", full) + if debugNetstack { + ns.logf("[v2] packet Write out: % x", full) + } if err := ns.tundev.InjectOutbound(full); err != nil { log.Printf("netstack inject outbound: %v", err) return @@ -304,7 +307,9 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.TUN) filter.Response { case 6: pn = header.IPv6ProtocolNumber } - ns.logf("[v2] packet in (from %v): % x", p.Src, p.Buffer()) + if debugNetstack { + ns.logf("[v2] packet in (from %v): % x", p.Src, p.Buffer()) + } vv := buffer.View(append([]byte(nil), p.Buffer()...)).ToVectorisedView() packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{ Data: vv, @@ -314,7 +319,11 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.TUN) filter.Response { } func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) { - ns.logf("[v2] ForwarderRequest: %v", r) + if debugNetstack { + // Kinda ugly: + // ForwarderRequest: &{{{{0 0}}} 0xc0001c30b0 0xc0004c3d40 {1240 6 true 826109390 0 true} + ns.logf("[v2] ForwarderRequest: %v", r) + } var wq waiter.Queue ep, err := r.CreateEndpoint(&wq) if err != nil { From 92cdb30b2639e0e59b3e5386fc1c7dd213a1249a Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 3 Mar 2021 10:17:05 -0800 Subject: [PATCH 33/48] tailcfg, control/controlclient: add goroutine dump debug feature Signed-off-by: Brad Fitzpatrick --- control/controlclient/debug.go | 69 +++++++++++++++++++++++++++++ control/controlclient/debug_test.go | 11 +++++ control/controlclient/direct.go | 3 ++ tailcfg/tailcfg.go | 4 ++ 4 files changed, 87 insertions(+) create mode 100644 control/controlclient/debug.go create mode 100644 control/controlclient/debug_test.go diff --git a/control/controlclient/debug.go b/control/controlclient/debug.go new file mode 100644 index 000000000..fe9d3450a --- /dev/null +++ b/control/controlclient/debug.go @@ -0,0 +1,69 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package controlclient + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "log" + "net/http" + "regexp" + "runtime" + "strconv" + "time" +) + +func dumpGoroutinesToURL(c *http.Client, targetURL string) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + zbuf := new(bytes.Buffer) + zw := gzip.NewWriter(zbuf) + zw.Write(scrubbedGoroutineDump()) + zw.Close() + + req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf) + if err != nil { + log.Printf("dumpGoroutinesToURL: %v", err) + return + } + req.Header.Set("Content-Encoding", "gzip") + t0 := time.Now() + _, err = c.Do(req) + d := time.Since(t0).Round(time.Millisecond) + if err != nil { + log.Printf("dumpGoroutinesToURL error: %v to %v (after %v)", err, targetURL, d) + } else { + log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d) + } +} + +var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`) + +// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual +// values of arguments scrubbed out, lest it contain some private key material. +func scrubbedGoroutineDump() []byte { + buf := make([]byte, 1<<20) + buf = buf[:runtime.Stack(buf, true)] + + saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8) + return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte { + if string(in) == "0x0" { + return in + } + if v, ok := saw[string(in)]; ok { + return v + } + u64, err := strconv.ParseUint(string(in[2:]), 16, 64) + if err != nil { + return []byte("??") + } + v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8)) + saw[string(in)] = v + return v + }) +} diff --git a/control/controlclient/debug_test.go b/control/controlclient/debug_test.go new file mode 100644 index 000000000..8e928789a --- /dev/null +++ b/control/controlclient/debug_test.go @@ -0,0 +1,11 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package controlclient + +import "testing" + +func TestScrubbedGoroutineDump(t *testing.T) { + t.Logf("Got:\n%s\n", scrubbedGoroutineDump()) +} diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 6aab70760..b38552e2d 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -702,6 +702,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm if resp.Debug.LogHeapPprof { go logheap.LogHeap(resp.Debug.LogHeapURL) } + if resp.Debug.GoroutineDumpURL != "" { + go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL) + } setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute) setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig) } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 6ad7b0c19..4907feb51 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -824,6 +824,10 @@ type Debug struct { // DisableSubnetsIfPAC controls whether subnet routers should be // disabled if WPAD is present on the network. DisableSubnetsIfPAC opt.Bool `json:",omitempty"` + + // GoroutineDumpURL, if non-empty, requests that the client do + // a one-time dump of its active goroutines to the given URL. + GoroutineDumpURL string `json:",omitempty"` } func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) } From 1cb0ffc3ffdcca856fca8f79afc4d853c0eb038a Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 2 Mar 2021 20:26:36 -0800 Subject: [PATCH 34/48] wgengine/router: make windows gracefully handle disabled IPv4 or IPv6. This is necessary because either protocol can be disabled globally by a Windows registry policy, at which point trying to touch that address family results in "Element not found" errors. This change skips programming address families that Windows tell us are unavailable. Fixes #1396. Signed-off-by: David Anderson --- wgengine/router/ifconfig_windows.go | 150 ++++++++++++++++++---------- 1 file changed, 95 insertions(+), 55 deletions(-) diff --git a/wgengine/router/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go index 4a331319f..b5d265526 100644 --- a/wgengine/router/ifconfig_windows.go +++ b/wgengine/router/ifconfig_windows.go @@ -51,24 +51,27 @@ func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, er if mtu > 0 && (lastMtu == 0 || lastMtu != mtu) { iface, err := ourLuid.IPInterface(windows.AF_INET) if err != nil { - return fmt.Errorf("error getting v4 interface: %w", err) + if !errors.Is(err, windows.ERROR_NOT_FOUND) { + return fmt.Errorf("getting v4 interface: %w", err) + } + } else { + iface.NLMTU = mtu - 80 + // If the TUN device was created with a smaller MTU, + // though, such as 1280, we don't want to go bigger + // than configured. (See the comment on minimalMTU in + // the wgengine package.) + if min, err := tun.MTU(); err == nil && min < int(iface.NLMTU) { + iface.NLMTU = uint32(min) + } + if iface.NLMTU < 576 { + iface.NLMTU = 576 + } + err = iface.Set() + if err != nil { + return fmt.Errorf("error setting v4 MTU: %w", err) + } + tun.ForceMTU(int(iface.NLMTU)) } - iface.NLMTU = mtu - 80 - // If the TUN device was created with a smaller MTU, - // though, such as 1280, we don't want to go bigger than - // configured. (See the comment on minimalMTU in the - // wgengine package.) - if min, err := tun.MTU(); err == nil && min < int(iface.NLMTU) { - iface.NLMTU = uint32(min) - } - if iface.NLMTU < 576 { - iface.NLMTU = 576 - } - err = iface.Set() - if err != nil { - return fmt.Errorf("error setting v4 MTU: %w", err) - } - tun.ForceMTU(int(iface.NLMTU)) iface, err = ourLuid.IPInterface(windows.AF_INET6) if err != nil { if !errors.Is(err, windows.ERROR_NOT_FOUND) { @@ -249,7 +252,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { winipcfg.GAAFlagIncludeAllInterfaces, ) if err != nil { - return err + return fmt.Errorf("getting interface: %w", err) } // Send non-nil return errors to retErrc, to interupt our background @@ -288,12 +291,42 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { log.Printf("setPrivateNetwork: adapter LUID %v not found after %d tries, giving up", luid, tries) }() + // Figure out which of IPv4 and IPv6 are available. Both protocols + // can be disabled on a per-interface basis by the user, as well + // as globally via a registry policy. We skip programming anything + // related to the disabled protocols, since by definition they're + // unusable. + ipif4, err := iface.LUID.IPInterface(windows.AF_INET) + if err != nil { + if !errors.Is(err, windows.ERROR_NOT_FOUND) { + return fmt.Errorf("getting AF_INET interface: %w", err) + } + log.Printf("AF_INET interface not found on Tailscale adapter, skipping IPv4 programming") + ipif4 = nil + } + ipif6, err := iface.LUID.IPInterface(windows.AF_INET6) + if err != nil { + if !errors.Is(err, windows.ERROR_NOT_FOUND) { + return fmt.Errorf("getting AF_INET6 interface: %w", err) + } + log.Printf("AF_INET6 interface not found on Tailscale adapter, skipping IPv6 programming") + ipif6 = nil + } + + // Windows requires routes to have a nexthop. For routes such as + // ours where the nexthop is meaningless, you're supposed to use + // one of the local IP addresses of the interface. Find an IPv4 + // and IPv6 address we can use for this purpose. var firstGateway4 *net.IP var firstGateway6 *net.IP - addresses := make([]*net.IPNet, len(cfg.LocalAddrs)) - for i, addr := range cfg.LocalAddrs { + addresses := make([]*net.IPNet, 0, len(cfg.LocalAddrs)) + for _, addr := range cfg.LocalAddrs { + if (addr.IP.Is4() && ipif4 == nil) || (addr.IP.Is6() && ipif6 == nil) { + // Can't program addresses for disabled protocol. + continue + } ipnet := addr.IPNet() - addresses[i] = ipnet + addresses = append(addresses, ipnet) gateway := ipnet.IP if addr.IP.Is4() && firstGateway4 == nil { firstGateway4 = &gateway @@ -306,6 +339,11 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { foundDefault4 := false foundDefault6 := false for _, route := range cfg.Routes { + if (route.IP.Is4() && ipif4 == nil) || (route.IP.Is6() && ipif6 == nil) { + // Can't program routes for disabled protocol. + continue + } + if route.IP.Is6() && firstGateway6 == nil { // Windows won't let us set IPv6 routes without having an // IPv6 local address set. However, when we've configured @@ -317,6 +355,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { addresses = append(addresses, ipnet) firstGateway6 = &ipnet.IP } else if route.IP.Is4() && firstGateway4 == nil { + // TODO: do same dummy behavior as v6? return errors.New("Due to a Windows limitation, one cannot have interface routes without an interface address") } @@ -359,7 +398,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { err = syncAddresses(iface, addresses) if err != nil { - return err + return fmt.Errorf("syncAddresses: %w", err) } sort.Slice(routes, func(i, j int) bool { return routeLess(&routes[i], &routes[j]) }) @@ -385,7 +424,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { winipcfg.GAAFlagIncludeAllInterfaces, ) if err != nil { - return err + return fmt.Errorf("getting interface: %w", err) } var errAcc error @@ -395,45 +434,46 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { errAcc = err } - ipif, err := iface.LUID.IPInterface(windows.AF_INET) - if err != nil { - log.Printf("getipif: %v", err) - return err - } - if foundDefault4 { - ipif.UseAutomaticMetric = false - ipif.Metric = 0 - } - if mtu > 0 { - ipif.NLMTU = uint32(mtu) - tun.ForceMTU(int(ipif.NLMTU)) - } - err = ipif.Set() - if err != nil && errAcc == nil { - errAcc = err - } - - ipif, err = iface.LUID.IPInterface(windows.AF_INET6) - if err != nil { - if !errors.Is(err, windows.ERROR_NOT_FOUND) { - return err + if ipif4 != nil { + ipif4, err = iface.LUID.IPInterface(windows.AF_INET) + if err != nil { + return fmt.Errorf("getting AF_INET interface: %w", err) } - } else { - if foundDefault6 { - ipif.UseAutomaticMetric = false - ipif.Metric = 0 + if foundDefault4 { + ipif4.UseAutomaticMetric = false + ipif4.Metric = 0 } if mtu > 0 { - ipif.NLMTU = uint32(mtu) + ipif4.NLMTU = uint32(mtu) + tun.ForceMTU(int(ipif4.NLMTU)) } - ipif.DadTransmits = 0 - ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled - err = ipif.Set() + err = ipif4.Set() if err != nil && errAcc == nil { errAcc = err } } + if ipif6 != nil { + ipif6, err = iface.LUID.IPInterface(windows.AF_INET6) + if err != nil { + return fmt.Errorf("getting AF_INET6 interface: %w", err) + } else { + if foundDefault6 { + ipif6.UseAutomaticMetric = false + ipif6.Metric = 0 + } + if mtu > 0 { + ipif6.NLMTU = uint32(mtu) + } + ipif6.DadTransmits = 0 + ipif6.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled + err = ipif6.Set() + if err != nil && errAcc == nil { + errAcc = err + } + } + } + return errAcc } @@ -576,14 +616,14 @@ func syncAddresses(ifc *winipcfg.IPAdapterAddresses, want []*net.IPNet) error { for _, a := range del { err := ifc.LUID.DeleteIPAddress(*a) if err != nil { - erracc = err + erracc = fmt.Errorf("deleting IP %q: %w", *a, err) } } for _, a := range add { err := ifc.LUID.AddIPAddress(*a) if err != nil { - erracc = err + erracc = fmt.Errorf("adding IP %q: %w", *a, err) } } From ea49b1e811befb7e3b26448dd8df011aaee07dde Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 3 Mar 2021 12:01:15 -0800 Subject: [PATCH 35/48] tailcfg: bump map request version for v6 + default routes. Signed-off-by: David Anderson --- tailcfg/tailcfg.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 4907feb51..2274d3cf4 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -31,10 +31,11 @@ import ( // 5: 2020-10-19, implies IncludeIPv6, delta Peers/UserProfiles, supports MagicDNS // 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged // 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/:: -// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side +// 8: 2020-12-19: client can buggily receive IPv6 addresses and routes if beta enabled server-side // 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains // 10: 2021-01-17: client understands MapResponse.PeerSeenChange -const CurrentMapRequestVersion = 10 +// 11: 2021-03-03: client understands IPv6 and multiple nodes with default routes +const CurrentMapRequestVersion = 11 type StableID string From 2e347d1e10070fe61e399c6a3f3c77ab5368568e Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 3 Mar 2021 15:06:35 -0800 Subject: [PATCH 36/48] tailcfg: tweak documentation for map version 11 version: bump date. --- tailcfg/tailcfg.go | 2 +- version/version.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 2274d3cf4..f9b9d57a0 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -34,7 +34,7 @@ import ( // 8: 2020-12-19: client can buggily receive IPv6 addresses and routes if beta enabled server-side // 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains // 10: 2021-01-17: client understands MapResponse.PeerSeenChange -// 11: 2021-03-03: client understands IPv6 and multiple nodes with default routes +// 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping const CurrentMapRequestVersion = 11 type StableID string diff --git a/version/version.go b/version/version.go index 90d3f5ff8..3aed30e58 100644 --- a/version/version.go +++ b/version/version.go @@ -10,7 +10,7 @@ package version // Long is a full version number for this build, of the form // "x.y.z-commithash", or "date.yyyymmdd" if no actual version was // provided. -const Long = "date.20210226" +const Long = "date.20210303" // Short is a short version number for this build, of the form // "x.y.z", or "date.yyyymmdd" if no actual version was provided. From 6756f206328f2e0947a4bd30131f3fd20888add7 Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Tue, 2 Mar 2021 12:48:29 -0800 Subject: [PATCH 37/48] go.mod: update peercred Adds FreeBSD support. Signed-off-by: Denton Gentry --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 38da8df50..9211c49e1 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 - golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 + golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 @@ -40,6 +40,6 @@ require ( gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6 honnef.co/go/tools v0.1.0 inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 - inet.af/peercred v0.0.0-20210216231719-993aa01eacaa + inet.af/peercred v0.0.0-20210302202138-56e694897155 rsc.io/goversion v1.2.0 ) diff --git a/go.sum b/go.sum index d560f1223..57311b408 100644 --- a/go.sum +++ b/go.sum @@ -449,6 +449,8 @@ golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b h1:kHlr0tATeLRMEiZJu5CknOw/E8V6h69sXXQFGoPtjcc= +golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q= @@ -565,6 +567,8 @@ inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1Ov inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4= inet.af/peercred v0.0.0-20210216231719-993aa01eacaa h1:6qseJO2iNDHl+MLL2BkO5oURJR4A9pLmRz11Yf7KdGM= inet.af/peercred v0.0.0-20210216231719-993aa01eacaa/go.mod h1:VZeNdG7cRIUqKl9DWoFX86AHyfYwdb4RextAw1CAEO4= +inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE= +inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs= k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ= k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ= From 524fb2c1908dc06b166f2a4fd2015d00b7d5ea92 Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Wed, 3 Mar 2021 11:41:32 -0800 Subject: [PATCH 38/48] safesocket: add FreeBSD to PlatformUsesPeerCreds FreeBSD is supported by peercred now. Signed-off-by: Denton Gentry --- safesocket/safesocket.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safesocket/safesocket.go b/safesocket/safesocket.go index e27cdb302..5769c2145 100644 --- a/safesocket/safesocket.go +++ b/safesocket/safesocket.go @@ -70,7 +70,7 @@ func LocalTCPPortAndToken() (port int, token string, err error) { // to authenticate connections. func PlatformUsesPeerCreds() bool { switch runtime.GOOS { - case "linux", "darwin": + case "linux", "darwin", "freebsd": return true } return false From 061422affcc4278d0400c248432f39ee60fcf16e Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Wed, 3 Mar 2021 11:34:37 -0800 Subject: [PATCH 39/48] freebsd: ignore IPv6 for now FreeBSD tun devices don't work with the way we implement IPv6 https://github.com/tailscale/tailscale/issues/1307 At least for now, remove any IPv6 addresses from the netmap. Signed-off-by: Denton Gentry --- wgengine/router/config_clone.go | 39 +++++++++++++++++++++++++ wgengine/router/dns/config.go | 2 ++ wgengine/router/dns/config_clone.go | 33 +++++++++++++++++++++ wgengine/router/router.go | 2 ++ wgengine/router/router_userspace_bsd.go | 32 ++++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 wgengine/router/config_clone.go create mode 100644 wgengine/router/dns/config_clone.go diff --git a/wgengine/router/config_clone.go b/wgengine/router/config_clone.go new file mode 100644 index 000000000..e35c21043 --- /dev/null +++ b/wgengine/router/config_clone.go @@ -0,0 +1,39 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by tailscale.com/cmd/cloner -type Config; DO NOT EDIT. + +package router + +import ( + "inet.af/netaddr" + "tailscale.com/types/preftype" + "tailscale.com/wgengine/router/dns" +) + +// Clone makes a deep copy of Config. +// The result aliases no memory with the original. +func (src *Config) Clone() *Config { + if src == nil { + return nil + } + dst := new(Config) + *dst = *src + dst.LocalAddrs = append(src.LocalAddrs[:0:0], src.LocalAddrs...) + dst.Routes = append(src.Routes[:0:0], src.Routes...) + dst.DNS = *src.DNS.Clone() + dst.SubnetRoutes = append(src.SubnetRoutes[:0:0], src.SubnetRoutes...) + return dst +} + +// A compilation failure here means this code must be regenerated, with command: +// tailscale.com/cmd/cloner -type Config +var _ConfigNeedsRegeneration = Config(struct { + LocalAddrs []netaddr.IPPrefix + Routes []netaddr.IPPrefix + DNS dns.Config + SubnetRoutes []netaddr.IPPrefix + SNATSubnetRoutes bool + NetfilterMode preftype.NetfilterMode +}{}) diff --git a/wgengine/router/dns/config.go b/wgengine/router/dns/config.go index 2b6ff615a..e1adfefce 100644 --- a/wgengine/router/dns/config.go +++ b/wgengine/router/dns/config.go @@ -10,6 +10,8 @@ import ( "tailscale.com/types/logger" ) +//go:generate go run tailscale.com/cmd/cloner -type=Config -output=config_clone.go + // Config is the set of parameters that uniquely determine // the state to which a manager should bring system DNS settings. type Config struct { diff --git a/wgengine/router/dns/config_clone.go b/wgengine/router/dns/config_clone.go new file mode 100644 index 000000000..42e91a6e1 --- /dev/null +++ b/wgengine/router/dns/config_clone.go @@ -0,0 +1,33 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by tailscale.com/cmd/cloner -type Config; DO NOT EDIT. + +package dns + +import ( + "inet.af/netaddr" +) + +// Clone makes a deep copy of Config. +// The result aliases no memory with the original. +func (src *Config) Clone() *Config { + if src == nil { + return nil + } + dst := new(Config) + *dst = *src + dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...) + dst.Domains = append(src.Domains[:0:0], src.Domains...) + return dst +} + +// A compilation failure here means this code must be regenerated, with command: +// tailscale.com/cmd/cloner -type Config +var _ConfigNeedsRegeneration = Config(struct { + Nameservers []netaddr.IP + Domains []string + PerDomain bool + Proxied bool +}{}) diff --git a/wgengine/router/router.go b/wgengine/router/router.go index 9c3f1003f..b8f626e7b 100644 --- a/wgengine/router/router.go +++ b/wgengine/router/router.go @@ -54,6 +54,8 @@ func Cleanup(logf logger.Logf, interfaceName string) { cleanup(logf, interfaceName) } +//go:generate go run tailscale.com/cmd/cloner -type=Config -output=config_clone.go + // Config is the subset of Tailscale configuration that is relevant to // the OS's network stack. type Config struct { diff --git a/wgengine/router/router_userspace_bsd.go b/wgengine/router/router_userspace_bsd.go index fb81d62fb..b867f8a4e 100644 --- a/wgengine/router/router_userspace_bsd.go +++ b/wgengine/router/router_userspace_bsd.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "os/exec" + "runtime" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" @@ -101,9 +102,40 @@ func inet(p netaddr.IPPrefix) string { return "inet" } +// See https://github.com/tailscale/tailscale/issues/1307#issuecomment-786045280 +// Remove all IPv6 entries. +func (r *userspaceBSDRouter) modifiedConfigForFreeBSDBugWorkaround(cfg *Config) *Config { + n := cfg.Clone() + + n.LocalAddrs = n.LocalAddrs[:0] + for _, addr := range cfg.LocalAddrs { + if !addr.IP.Is6() { + n.LocalAddrs = append(n.LocalAddrs, addr) + } + } + + n.Routes = n.Routes[:0] + for _, addr := range cfg.Routes { + if !addr.IP.Is6() { + n.Routes = append(n.Routes, addr) + } + } + + n.SubnetRoutes = n.SubnetRoutes[:0] + for _, addr := range cfg.SubnetRoutes { + if !addr.IP.Is6() { + n.SubnetRoutes = append(n.SubnetRoutes, addr) + } + } + + return n +} + func (r *userspaceBSDRouter) Set(cfg *Config) (reterr error) { if cfg == nil { cfg = &shutdownConfig + } else if runtime.GOOS == "freebsd" { + cfg = r.modifiedConfigForFreeBSDBugWorkaround(cfg) } var errq error From 10f48087f49b11e8401c79bd1c198c65a5306134 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Mar 2021 14:23:14 -0800 Subject: [PATCH 40/48] net/tshttpproxy: call winhttp calls from a fixed OS thread We often see things in logs like: 2021-03-02 17:52:45.2456258 +0800 +0800: winhttp: Open: The parameter is incorrect. 2021-03-02 17:52:45.2506261 +0800 +0800: tshttpproxy: winhttp: GetProxyForURL("https://log.tailscale.io/c/tailnode.log.tailscale.io/5037bb42f4bc330e2d6143e191a7ff7e837c6be538139231de69a439536e0d68"): ERROR_INVALID_PARAMETER [unexpected] I have a hunch that WinHTTP has thread-local state. If so, this would fix it. If not, this is pretty harmless. Signed-off-by: Brad Fitzpatrick --- net/tshttpproxy/tshttpproxy_windows.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/net/tshttpproxy/tshttpproxy_windows.go b/net/tshttpproxy/tshttpproxy_windows.go index bd85d2104..ac0415d51 100644 --- a/net/tshttpproxy/tshttpproxy_windows.go +++ b/net/tshttpproxy/tshttpproxy_windows.go @@ -11,6 +11,7 @@ import ( "log" "net/http" "net/url" + "runtime" "strings" "sync" "syscall" @@ -109,6 +110,9 @@ func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) { } func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + whi, err := winHTTPOpen() if err != nil { proxyErrorf("winhttp: Open: %v", err) From ffa70a617dd0ac29c10f886f70ea47e674f29fb6 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 3 Mar 2021 20:58:09 -0800 Subject: [PATCH 41/48] wgengine{,/monitor}: restore Engine.LinkChange, add Mon.InjectEvent The Engine.LinkChange method was recently removed in e3df29d488f5ce50ee396b1f05a92e9cf1abb006 while misremembering how Android's link state mechanism worked. Rather than do some last minute rearchitecting of link state on Android before Tailscale 1.6, restore the old Engine.LinkChange hook for now so the Android client doesn't need any changes. But change how it's implemented to instead inject an event into the link monitor. Fixes #1427 Signed-off-by: Brad Fitzpatrick --- wgengine/monitor/monitor.go | 22 +++++++++++++++------- wgengine/monitor/monitor_test.go | 24 ++++++++++++++++++++++++ wgengine/userspace.go | 6 ++++++ wgengine/watchdog.go | 3 +++ wgengine/wgengine.go | 15 +++++++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 9f8610a06..da99a9d6c 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -148,6 +148,20 @@ func (m *Mon) Close() error { return err } +// InjectEvent forces the monitor to pretend there was a network +// change and re-check the state of the network. Any registered +// ChangeFunc callbacks will be called within the event coalescing +// period (under a fraction of a second). +func (m *Mon) InjectEvent() { + select { + case m.change <- struct{}{}: + default: + // Another change signal is already + // buffered. Debounce will wake up soon + // enough. + } +} + func (m *Mon) stopped() bool { select { case <-m.stop: @@ -175,13 +189,7 @@ func (m *Mon) pump() { if msg.ignore() { continue } - select { - case m.change <- struct{}{}: - default: - // Another change signal is already - // buffered. Debounce will wake up soon - // enough. - } + m.InjectEvent() } } diff --git a/wgengine/monitor/monitor_test.go b/wgengine/monitor/monitor_test.go index f1e35439e..9dd012ded 100644 --- a/wgengine/monitor/monitor_test.go +++ b/wgengine/monitor/monitor_test.go @@ -7,6 +7,7 @@ package monitor import ( "flag" "testing" + "time" "tailscale.com/net/interfaces" ) @@ -32,6 +33,29 @@ func TestMonitorJustClose(t *testing.T) { } } +func TestMonitorInjectEvent(t *testing.T) { + mon, err := New(t.Logf) + if err != nil { + t.Fatal(err) + } + defer mon.Close() + got := make(chan bool, 1) + mon.RegisterChangeCallback(func(changed bool, state *interfaces.State) { + select { + case got <- true: + default: + } + }) + mon.Start() + mon.InjectEvent() + select { + case <-got: + // Pass. + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for callback") + } +} + var monitor = flag.String("monitor", "", `go into monitor mode like 'route monitor'; test never terminates. Value can be either "raw" or "callback"`) func TestMonitorMode(t *testing.T) { diff --git a/wgengine/userspace.go b/wgengine/userspace.go index d61b9645d..8d73e9483 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -1251,6 +1251,12 @@ func (e *userspaceEngine) GetLinkMonitor() *monitor.Mon { return e.linkMon } +// LinkChange signals a network change event. It's currently +// (2021-03-03) only called on Android. +func (e *userspaceEngine) LinkChange(_ bool) { + e.linkMon.InjectEvent() +} + func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) { up := cur.AnyInterfaceUp() if !up { diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index bcef4e160..3b96f2e8e 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -99,6 +99,9 @@ func (e *watchdogEngine) SetNetInfoCallback(cb NetInfoCallback) { func (e *watchdogEngine) RequestStatus() { e.watchdog("RequestStatus", func() { e.wrap.RequestStatus() }) } +func (e *watchdogEngine) LinkChange(isExpensive bool) { + e.watchdog("LinkChange", func() { e.wrap.LinkChange(isExpensive) }) +} func (e *watchdogEngine) SetDERPMap(m *tailcfg.DERPMap) { e.watchdog("SetDERPMap", func() { e.wrap.SetDERPMap(m) }) } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index a72d0a96a..57bc7cb77 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -89,6 +89,21 @@ type Engine interface { // TODO: return an error? Wait() + // LinkChange informs the engine that the system network + // link has changed. + // + // The isExpensive parameter is not used. + // + // LinkChange should be called whenever something changed with + // the network, no matter how minor. + // + // Deprecated: don't use this method. It was removed shortly + // before the Tailscale 1.6 release when we remembered that + // Android doesn't use the Linux-based link monitor and has + // its own mechanism that uses LinkChange. Android is the only + // caller of this method now. Don't add more. + LinkChange(isExpensive bool) + // SetDERPMap controls which (if any) DERP servers are used. // If nil, DERP is disabled. It starts disabled until a DERP map // is configured. From ad6edf5ecda94ac89df8278871a29c3783ae85e4 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 3 Mar 2021 19:00:41 -0800 Subject: [PATCH 42/48] portlist: report a better process name for .Net on linux. Fixes #1440. Signed-off-by: David Anderson --- portlist/clean.go | 34 ++++++++++++++++++++++++++++++ portlist/clean_test.go | 42 ++++++++++++++++++++++++++++++++++++++ portlist/netstat.go | 8 ++------ portlist/portlist_linux.go | 9 ++++---- 4 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 portlist/clean.go create mode 100644 portlist/clean_test.go diff --git a/portlist/clean.go b/portlist/clean.go new file mode 100644 index 000000000..aad350fe3 --- /dev/null +++ b/portlist/clean.go @@ -0,0 +1,34 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package portlist + +import ( + "path/filepath" + "strings" +) + +// argvSubject takes a command and its flags, and returns the +// short/pretty name for the process. This is usually the basename of +// the binary being executed, but can sometimes vary (e.g. so that we +// don't report all Java programs as "java"). +func argvSubject(argv ...string) string { + if len(argv) == 0 { + return "" + } + ret := filepath.Base(argv[0]) + + // Handle special cases. + switch { + case ret == "mono" && len(argv) >= 2: + // .Net programs execute as `mono actualProgram.exe`. + ret = filepath.Base(argv[1]) + } + + // Remove common noise. + ret = strings.TrimSpace(ret) + ret = strings.TrimSuffix(ret, ".exe") + + return ret +} diff --git a/portlist/clean_test.go b/portlist/clean_test.go new file mode 100644 index 000000000..767071481 --- /dev/null +++ b/portlist/clean_test.go @@ -0,0 +1,42 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package portlist + +import "testing" + +func TestArgvSubject(t *testing.T) { + tests := []struct { + in []string + want string + }{ + { + in: nil, + want: "", + }, + { + in: []string{"/usr/bin/sshd"}, + want: "sshd", + }, + { + in: []string{"/bin/mono"}, + want: "mono", + }, + { + in: []string{"/nix/store/x2cw2xjw98zdysf56bdlfzsr7cyxv0jf-mono-5.20.1.27/bin/mono", "/bin/exampleProgram.exe"}, + want: "exampleProgram", + }, + { + in: []string{"/bin/mono", "/sbin/exampleProgram.bin"}, + want: "exampleProgram.bin", + }, + } + + for _, test := range tests { + got := argvSubject(test.in...) + if got != test.want { + t.Errorf("argvSubject(%v) = %q, want %q", test.in, got, test.want) + } + } +} diff --git a/portlist/netstat.go b/portlist/netstat.go index 02b1a5957..7eb8ed973 100644 --- a/portlist/netstat.go +++ b/portlist/netstat.go @@ -101,13 +101,9 @@ func parsePortsNetstat(output string) List { delete(m, lastport) proc := trimline[1 : len(trimline)-1] if proc == "svchost.exe" && lastline != "" { - p.Process = lastline + p.Process = argvSubject(lastline) } else { - if strings.HasSuffix(proc, ".exe") { - p.Process = proc[:len(proc)-4] - } else { - p.Process = proc - } + p.Process = argvSubject(proc) } m[p] = nothing{} } else { diff --git a/portlist/portlist_linux.go b/portlist/portlist_linux.go index 8b7555872..cc22b3f1a 100644 --- a/portlist/portlist_linux.go +++ b/portlist/portlist_linux.go @@ -158,18 +158,17 @@ func addProcesses(pl []Port) ([]Port, error) { continue } - // TODO(apenwarr): use /proc/*/cmdline instead of /comm? - // Unsure right now whether users will want the extra detail - // or not. pe := pm[string(targetBuf[:n])] // m[string([]byte)] avoids alloc if pe != nil { - comm, err := ioutil.ReadFile(fmt.Sprintf("/proc/%s/comm", pid)) + bs, err := ioutil.ReadFile(fmt.Sprintf("/proc/%s/cmdline", pid)) if err != nil { // Usually shouldn't happen. One possibility is // the process has gone away, so let's skip it. continue } - pe.Process = strings.TrimSpace(string(comm)) + + argv := strings.Split(strings.TrimSuffix(string(bs), "\x00"), "\x00") + pe.Process = argvSubject(argv...) } } } From 829eb8363a1fd837661f6e16c3e8de4c5783854b Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 3 Mar 2021 22:02:45 -0800 Subject: [PATCH 43/48] net/interfaces: sort returned addresses from LocalAddresses Also change the type to netaddr.IP while here, because it made sorting easier. Updates tailscale/corp#1397 Signed-off-by: Brad Fitzpatrick --- net/interfaces/interfaces.go | 12 +++++++++--- wgengine/magicsock/magicsock.go | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index 02afd44ba..4d48ef6be 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -78,7 +78,7 @@ func isProblematicInterface(nif *net.Interface) bool { // LocalAddresses returns the machine's IP addresses, separated by // whether they're loopback addresses. -func LocalAddresses() (regular, loopback []string, err error) { +func LocalAddresses() (regular, loopback []netaddr.IP, err error) { // TODO(crawshaw): don't serve interface addresses that we are routing ifaces, err := net.Interfaces() if err != nil { @@ -117,16 +117,22 @@ func LocalAddresses() (regular, loopback []string, err error) { continue } if ip.IsLoopback() || ifcIsLoopback { - loopback = append(loopback, ip.String()) + loopback = append(loopback, ip) } else { - regular = append(regular, ip.String()) + regular = append(regular, ip) } } } } + sortIPs(regular) + sortIPs(loopback) return regular, loopback, nil } +func sortIPs(s []netaddr.IP) { + sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) }) +} + // Interface is a wrapper around Go's net.Interface with some extra methods. type Interface struct { *net.Interface diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index bcb6f750b..ff8051b96 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -1055,8 +1055,8 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason ips = loopback reason = "loopback" } - for _, ipStr := range ips { - addAddr(net.JoinHostPort(ipStr, fmt.Sprint(localAddr.Port)), reason) + for _, ip := range ips { + addAddr(netaddr.IPPort{IP: ip, Port: uint16(localAddr.Port)}.String(), reason) } } else { // Our local endpoint is bound to a particular address. From a6d098c750536fa26ead1f41943762667de7d132 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 4 Mar 2021 09:19:45 -0800 Subject: [PATCH 44/48] wgengine/magicsock: log when DERP connection succeeds Updates #1310 --- wgengine/magicsock/magicsock.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index ff8051b96..8b14a5c9e 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -1411,7 +1411,7 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d peerPresent := map[key.Public]bool{} bo := backoff.NewBackoff(fmt.Sprintf("derp-%d", regionID), c.logf, 5*time.Second) for { - msg, err := dc.Recv() + msg, connGen, err := dc.RecvDetail() if err != nil { // Forget that all these peers have routes. for peer := range peerPresent { @@ -1449,6 +1449,9 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d bo.BackOff(ctx, nil) // reset switch m := msg.(type) { + case derp.ServerInfoMessage: + c.logf("magicsock: derp-%d connected; connGen=%v", regionID, connGen) + continue case derp.ReceivedPacket: pkt = m res.n = len(m.Data) From 82edf94df72a52bcdc95fe37b20217e6003d92c0 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 4 Mar 2021 12:04:31 -0800 Subject: [PATCH 45/48] ipn/ipnlocal: make IPv6 OS routes be a single /48 for our ULA space And if we have over 10,000 CGNAT routes, just route the entire CGNAT range. (for the hello test server) Fixes #1450 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 46 +++++++++++++++++-- ipn/ipnlocal/local_test.go | 94 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index e3a60515a..1d1502605 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1362,6 +1362,41 @@ var ( ipv6Default = netaddr.MustParseIPPrefix("::/0") ) +// peerRoutes returns the routerConfig.Routes to access peers. +// If there are over cgnatThreshold CGNAT routes, one big CGNAT route +// is used instead. +func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netaddr.IPPrefix) { + tsULA := tsaddr.TailscaleULARange() + cgNAT := tsaddr.CGNATRange() + var didULA bool + var cgNATIPs []netaddr.IPPrefix + for _, peer := range peers { + for _, aip := range peer.AllowedIPs { + aip = unmapIPPrefix(aip) + // Only add the Tailscale IPv6 ULA once, if we see anybody using part of it. + if aip.IP.Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP) { + if !didULA { + didULA = true + routes = append(routes, tsULA) + } + continue + } + if aip.IsSingleIP() && cgNAT.Contains(aip.IP) { + cgNATIPs = append(cgNATIPs, aip) + } else { + routes = append(routes, aip) + } + } + } + if len(cgNATIPs) > cgnatThreshold { + // Probably the hello server. Just append one big route. + routes = append(routes, cgNAT) + } else { + routes = append(routes, cgNATIPs...) + } + return routes +} + // routerConfig produces a router.Config from a wireguard config and IPN prefs. func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { rs := &router.Config{ @@ -1369,10 +1404,7 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes), SNATSubnetRoutes: !prefs.NoSNAT, NetfilterMode: prefs.NetfilterMode, - } - - for _, peer := range cfg.Peers { - rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...) + Routes: peerRoutes(cfg.Peers, 10_000), } // Sanity check: we expect the control server to program both a v4 @@ -1409,10 +1441,14 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { return rs } +func unmapIPPrefix(ipp netaddr.IPPrefix) netaddr.IPPrefix { + return netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits} +} + func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) { for _, ipps := range ippsList { for _, ipp := range ipps { - ret = append(ret, netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits}) + ret = append(ret, unmapIPPrefix(ipp)) } } return ret diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index f8c1d88df..667cc2287 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -5,12 +5,14 @@ package ipnlocal import ( + "reflect" "testing" "inet.af/netaddr" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/netmap" + "tailscale.com/wgengine/wgcfg" ) func TestNetworkMapCompare(t *testing.T) { @@ -171,3 +173,95 @@ func TestShrinkDefaultRoute(t *testing.T) { } } } + +func TestPeerRoutes(t *testing.T) { + pp := netaddr.MustParseIPPrefix + tests := []struct { + name string + peers []wgcfg.Peer + want []netaddr.IPPrefix + }{ + { + name: "small_v4", + peers: []wgcfg.Peer{ + { + AllowedIPs: []netaddr.IPPrefix{ + pp("100.101.102.103/32"), + }, + }, + }, + want: []netaddr.IPPrefix{ + pp("100.101.102.103/32"), + }, + }, + { + name: "big_v4", + peers: []wgcfg.Peer{ + { + AllowedIPs: []netaddr.IPPrefix{ + pp("100.101.102.103/32"), + pp("100.101.102.104/32"), + pp("100.101.102.105/32"), + }, + }, + }, + want: []netaddr.IPPrefix{ + pp("100.64.0.0/10"), + }, + }, + { + name: "has_1_v6", + peers: []wgcfg.Peer{ + { + AllowedIPs: []netaddr.IPPrefix{ + pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"), + }, + }, + }, + want: []netaddr.IPPrefix{ + pp("fd7a:115c:a1e0::/48"), + }, + }, + { + name: "has_2_v6", + peers: []wgcfg.Peer{ + { + AllowedIPs: []netaddr.IPPrefix{ + pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"), + pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"), + }, + }, + }, + want: []netaddr.IPPrefix{ + pp("fd7a:115c:a1e0::/48"), + }, + }, + { + name: "big_v4_big_v6", + peers: []wgcfg.Peer{ + { + AllowedIPs: []netaddr.IPPrefix{ + pp("100.101.102.103/32"), + pp("100.101.102.104/32"), + pp("100.101.102.105/32"), + pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"), + pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"), + }, + }, + }, + want: []netaddr.IPPrefix{ + pp("fd7a:115c:a1e0::/48"), + pp("100.64.0.0/10"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := peerRoutes(tt.peers, 2) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("got = %v; want %v", got, tt.want) + } + }) + } + +} From 63a9adeb6c9258279ef367ea77c4ceb91cc347da Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 3 Mar 2021 20:12:09 -0800 Subject: [PATCH 46/48] portlist: collect IPv6 listening sockets on linux. This is important because some of those v6 sockets are actually dual-stacked sockets, so this is our only chance of discovering some services. Fixes #1443. Signed-off-by: David Anderson --- portlist/portlist_linux.go | 112 ++++++++++++++++++++------------ portlist/portlist_linux_test.go | 67 +++++++++++++++++++ 2 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 portlist/portlist_linux_test.go diff --git a/portlist/portlist_linux.go b/portlist/portlist_linux.go index cc22b3f1a..0400be203 100644 --- a/portlist/portlist_linux.go +++ b/portlist/portlist_linux.go @@ -10,6 +10,7 @@ import ( "io" "io/ioutil" "os" + "path/filepath" "runtime" "sort" "strconv" @@ -26,19 +27,25 @@ const pollInterval = 1 * time.Second // TODO(apenwarr): Include IPv6 ports eventually. // Right now we don't route IPv6 anyway so it's better to exclude them. -var sockfiles = []string{"/proc/net/tcp", "/proc/net/udp"} -var protos = []string{"tcp", "udp"} +var sockfiles = []string{"/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6"} var sawProcNetPermissionErr syncs.AtomicBool +const ( + v6Localhost = "00000000000000000000000001000000:" + v6Any = "00000000000000000000000000000000:0000" + v4Localhost = "0100007F:" + v4Any = "00000000:0000" +) + func listPorts() (List, error) { if sawProcNetPermissionErr.Get() { return nil, nil } l := []Port{} - for pi, fname := range sockfiles { - proto := protos[pi] + for _, fname := range sockfiles { + proto := strings.TrimSuffix(filepath.Base(fname), "6") // Android 10+ doesn't allow access to this anymore. // https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem @@ -59,47 +66,12 @@ func listPorts() (List, error) { defer f.Close() r := bufio.NewReader(f) - // skip header row - _, err = r.ReadString('\n') + ports, err := parsePorts(r, proto) if err != nil { - return nil, err + return nil, fmt.Errorf("parsing %q: %w", fname, err) } - for err == nil { - line, err := r.ReadString('\n') - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - - // sl local rem ... inode - words := strings.Fields(line) - local := words[1] - rem := words[2] - inode := words[9] - - // If a port is bound to 127.0.0.1, ignore it. - if strings.HasPrefix(local, "0100007F:") { - continue - } - if rem != "00000000:0000" { - // not a "listener" port - continue - } - - portv, err := strconv.ParseUint(local[9:], 16, 16) - if err != nil { - return nil, fmt.Errorf("%#v: %s", local[9:], err) - } - inodev := fmt.Sprintf("socket:[%s]", inode) - l = append(l, Port{ - Proto: proto, - Port: uint16(portv), - inode: inodev, - }) - } + l = append(l, ports...) } sort.Slice(l, func(i, j int) bool { @@ -109,6 +81,62 @@ func listPorts() (List, error) { return l, nil } +func parsePorts(r *bufio.Reader, proto string) ([]Port, error) { + var ret []Port + + // skip header row + _, err := r.ReadString('\n') + if err != nil { + return nil, err + } + + for err == nil { + line, err := r.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + // sl local rem ... inode + words := strings.Fields(line) + local := words[1] + rem := words[2] + inode := words[9] + + // If a port is bound to localhost, ignore it. + // TODO: localhost is bigger than 1 IP, we need to ignore + // more things. + if strings.HasPrefix(local, v4Localhost) || strings.HasPrefix(local, v6Localhost) { + continue + } + if rem != v4Any && rem != v6Any { + // not a "listener" port + continue + } + + // Don't use strings.Split here, because it causes + // allocations significant enough to show up in profiles. + i := strings.IndexByte(local, ':') + if i == -1 { + return nil, fmt.Errorf("%q unexpectedly didn't have a colon", local) + } + portv, err := strconv.ParseUint(local[i+1:], 16, 16) + if err != nil { + return nil, fmt.Errorf("%#v: %s", local[9:], err) + } + inodev := fmt.Sprintf("socket:[%s]", inode) + ret = append(ret, Port{ + Proto: proto, + Port: uint16(portv), + inode: inodev, + }) + } + + return ret, nil +} + func addProcesses(pl []Port) ([]Port, error) { pm := map[string]*Port{} // by Port.inode for i := range pl { diff --git a/portlist/portlist_linux_test.go b/portlist/portlist_linux_test.go new file mode 100644 index 000000000..aac2028f9 --- /dev/null +++ b/portlist/portlist_linux_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package portlist + +import ( + "bufio" + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParsePorts(t *testing.T) { + tests := []struct { + name string + in string + want []Port + }{ + { + name: "empty", + in: "header line (ignored)\n", + want: nil, + }, + { + name: "ipv4", + in: `header line + 0: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 22303 1 0000000000000000 100 0 0 10 0 + 1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 34062 1 0000000000000000 100 0 0 10 0 + 2: 5501A8C0:ADD4 B25E9536:01BB 01 00000000:00000000 02:00000B2B 00000000 1000 0 155276677 2 0000000000000000 22 4 30 10 -1 +`, + want: []Port{ + {Proto: "tcp", Port: 22, inode: "socket:[34062]"}, + }, + }, + { + name: "ipv6", + in: ` sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000000000000000000001000000:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 35720 1 0000000000000000 100 0 0 10 0 + 1: 00000000000000000000000000000000:1F91 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 142240557 1 0000000000000000 100 0 0 10 0 + 2: 00000000000000000000000000000000:0016 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 34064 1 0000000000000000 100 0 0 10 0 + 3: 69050120005716BC64906EBE009ECD4D:D506 0047062600000000000000006E171268:01BB 01 00000000:00000000 02:0000009E 00000000 1000 0 151042856 2 0000000000000000 21 4 28 10 -1 +`, + want: []Port{ + {Proto: "tcp", Port: 8081, inode: "socket:[142240557]"}, + {Proto: "tcp", Port: 22, inode: "socket:[34064]"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + buf := bytes.NewBufferString(test.in) + r := bufio.NewReader(buf) + + got, err := parsePorts(r, "tcp") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, test.want, cmp.AllowUnexported(Port{})); diff != "" { + t.Errorf("unexpected parsed ports (-got+want):\n%s", diff) + } + }) + } +} From d37b3b02cdcb75aebb721f902504dda41ccd88d0 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 4 Mar 2021 19:11:36 -0800 Subject: [PATCH 47/48] net/dnsfallback: fix infinite loop and limit number of candidates Updates #1455 (fixes the DNS spin part, but other things aren't ideal there) Signed-off-by: Brad Fitzpatrick --- net/dnsfallback/dnsfallback.go | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/net/dnsfallback/dnsfallback.go b/net/dnsfallback/dnsfallback.go index 9039dee1e..c71cb16ba 100644 --- a/net/dnsfallback/dnsfallback.go +++ b/net/dnsfallback/dnsfallback.go @@ -30,26 +30,40 @@ func Lookup(ctx context.Context, host string) ([]netaddr.IP, error) { ip netaddr.IP } - var cands []nameIP dm := derpmap.Prod() + var cands4, cands6 []nameIP for _, dr := range dm.Regions { for _, n := range dr.Nodes { if ip, err := netaddr.ParseIP(n.IPv4); err == nil { - cands = append(cands, nameIP{n.HostName, ip}) + cands4 = append(cands4, nameIP{n.HostName, ip}) } if ip, err := netaddr.ParseIP(n.IPv6); err == nil { - cands = append(cands, nameIP{n.HostName, ip}) + cands6 = append(cands6, nameIP{n.HostName, ip}) } } } - rand.Shuffle(len(cands), func(i, j int) { - cands[i], cands[j] = cands[j], cands[i] - }) + rand.Shuffle(len(cands4), func(i, j int) { cands4[i], cands4[j] = cands4[j], cands4[i] }) + rand.Shuffle(len(cands6), func(i, j int) { cands6[i], cands6[j] = cands6[j], cands6[i] }) + + const maxCands = 6 + var cands []nameIP // up to maxCands alternating v4/v6 as long as we have both + for (len(cands4) > 0 || len(cands6) > 0) && len(cands) < maxCands { + if len(cands4) > 0 { + cands = append(cands, cands4[0]) + cands4 = cands4[1:] + } + if len(cands6) > 0 { + cands = append(cands, cands6[0]) + cands6 = cands6[1:] + } + } if len(cands) == 0 { return nil, fmt.Errorf("no DNS fallback options for %q", host) } - for ctx.Err() == nil && len(cands) > 0 { - cand := cands[0] + for _, cand := range cands { + if err := ctx.Err(); err != nil { + return nil, err + } log.Printf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host) ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() From affd859121e331624af53fcc76363ba93949efde Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 4 Mar 2021 20:11:55 -0800 Subject: [PATCH 48/48] ipn/ipnlocal, control/controlclient: propagate link monitor to controlclient Don't use it yet, but get it down there. Updates #1455 Signed-off-by: Brad Fitzpatrick --- control/controlclient/direct.go | 4 ++++ ipn/ipnlocal/local.go | 1 + 2 files changed, 5 insertions(+) diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index b38552e2d..3e77d0a99 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -49,6 +49,7 @@ import ( "tailscale.com/util/systemd" "tailscale.com/version" "tailscale.com/wgengine/filter" + "tailscale.com/wgengine/monitor" ) // Direct is the client that connects to a tailcontrol server for a node. @@ -60,6 +61,7 @@ type Direct struct { newDecompressor func() (Decompressor, error) keepAlive bool logf logger.Logf + linkMon *monitor.Mon // or nil discoPubKey tailcfg.DiscoKey machinePrivKey wgkey.Private debugFlags []string @@ -91,6 +93,7 @@ type Options struct { Logf logger.Logf HTTPTestClient *http.Client // optional HTTP client to use (for tests only) DebugFlags []string // debug settings to send to control + LinkMonitor *monitor.Mon // optional link monitor // KeepSharerAndUserSplit controls whether the client // understands Node.Sharer. If false, the Sharer is mapped to the User. @@ -155,6 +158,7 @@ func NewDirect(opts Options) (*Direct, error) { discoPubKey: opts.DiscoPublicKey, debugFlags: opts.DebugFlags, keepSharerAndUserSplit: opts.KeepSharerAndUserSplit, + linkMon: opts.LinkMonitor, } if opts.Hostinfo == nil { c.SetHostinfo(NewHostinfo()) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 1d1502605..c17ed86b0 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -554,6 +554,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error { HTTPTestClient: opts.HTTPTestClient, DiscoPublicKey: discoPublic, DebugFlags: controlDebugFlags, + LinkMonitor: b.e.GetLinkMonitor(), }) if err != nil { return err