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/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 c31ccb703..fc7343150 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -7,21 +7,18 @@ package main // import "tailscale.com/cmd/hello" import ( "context" + _ "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 ( @@ -30,10 +27,13 @@ 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 != "" { - res, err := whoIs(*testIP) + res, err := tailscale.WhoIs(context.Background(), *testIP) if err != nil { log.Fatal(err) } @@ -42,8 +42,18 @@ func main() { e.Encode(res) return } - if !devMode() { - tmpl = template.Must(template.New("home").Parse(slurpHTML())) + 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+") + } + tmpl = template.Must(template.New("home").Parse(embeddedTemplate)) } http.HandleFunc("/", root) @@ -69,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" @@ -110,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") @@ -122,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() { @@ -136,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, @@ -161,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 -} 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) 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/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index c0009e52b..1399cd4e3 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -89,14 +89,15 @@ 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/flowtrack from tailscale.com/net/packet+ + 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 tailscale.com/net/netns from tailscale.com/control/controlclient+ 💣 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+ @@ -107,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/cmd/tailscaled+ + 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.go b/cmd/tailscaled/tailscaled.go index c4eb331c8..59204b16e 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,24 @@ 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/monitor" "tailscale.com/wgengine/netstack" "tailscale.com/wgengine/router" + "tailscale.com/wgengine/tstun" ) // globalStateKey is the ipn.StateKey that tailscaled loads on @@ -62,13 +68,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 +100,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") @@ -131,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" { @@ -190,23 +196,80 @@ 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 - } - e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl) - } else { - e, err = wgengine.NewUserspaceEngine(logf, wgengine.Config{ - TUNName: args.tunname, - ListenPort: args.port, - }) + 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 + socksListener, err = net.Listen("tcp", args.socksAddr) + if err != nil { + log.Fatalf("SOCKS5 listener: %v", err) + } + } + + conf := wgengine.Config{ + ListenPort: args.port, + LinkMonitor: linkMon, + } + if useTUN() { + conf.TUNName = args.tunname + } else { + conf.TUN = tstun.NewFakeTUN() + conf.RouterGen = router.NewFake + } + + e, err := wgengine.NewUserspaceEngine(logf, conf) if err != nil { logf("wgengine.New: %v", err) return err } + + var ns *netstack.Impl + if useNetstack() { + 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 useNetstack() { + 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()) @@ -266,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() } 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/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 66b529ec6..d85b669d7 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -38,6 +38,7 @@ import ( "tailscale.com/logpolicy" "tailscale.com/logtail" "tailscale.com/net/dnscache" + "tailscale.com/net/dnsfallback" "tailscale.com/net/netns" "tailscale.com/net/tlsdial" "tailscale.com/net/tshttpproxy" @@ -50,6 +51,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. @@ -61,6 +63,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 @@ -92,6 +95,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. @@ -128,16 +132,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} } @@ -154,6 +160,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()) @@ -731,6 +738,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/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= diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c9a4ebd2a..c17ed86b0 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,14 +139,19 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge portpoll: portpoll, gotPortPollRes: make(chan struct{}), } - e.SetLinkChangeCallback(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() @@ -169,6 +175,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 @@ -178,6 +188,7 @@ func (b *LocalBackend) Shutdown() { cli := b.c b.mu.Unlock() + b.unregisterLinkMon() if cli != nil { cli.Shutdown() } @@ -543,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 @@ -604,9 +616,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() @@ -632,6 +657,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 { @@ -1302,6 +1363,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{ @@ -1309,10 +1405,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 @@ -1349,10 +1442,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 9f19a08f1..667cc2287 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -5,11 +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) { @@ -118,3 +121,147 @@ 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) + } + } + } +} + +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) + } + }) + } + +} 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.go b/ipn/ipnserver/server.go index b37fee0ee..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" @@ -319,6 +320,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") @@ -333,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": 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/logtail/logtail.go b/logtail/logtail.go index bad60d011..d9ed73532 100644 --- a/logtail/logtail.go +++ b/logtail/logtail.go @@ -18,7 +18,9 @@ import ( "time" "tailscale.com/logtail/backoff" + "tailscale.com/net/interfaces" tslogger "tailscale.com/types/logger" + "tailscale.com/wgengine/monitor" ) // DefaultHost is the default host name to upload logs to when @@ -106,6 +108,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 +131,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. // @@ -277,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) @@ -293,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. 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..c71cb16ba --- /dev/null +++ b/net/dnsfallback/dnsfallback.go @@ -0,0 +1,117 @@ +// 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 + } + + 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 { + cands4 = append(cands4, nameIP{n.HostName, ip}) + } + if ip, err := netaddr.ParseIP(n.IPv6); err == nil { + cands6 = append(cands6, nameIP{n.HostName, ip}) + } + } + } + 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 _, 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() + 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 diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index a0843ab99..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 @@ -135,8 +141,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 +158,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 +167,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 +181,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 +199,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 +256,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 +301,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 +340,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 +389,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 +426,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 +467,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 } } 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 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 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/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/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) 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..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 { @@ -158,18 +186,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...) } } } 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) + } + }) + } +} diff --git a/safesocket/safesocket.go b/safesocket/safesocket.go index 5cbf73770..5769c2145 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", "freebsd": + 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. diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 6ad7b0c19..f9b9d57a0 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, multiple default routes, and goroutine dumping +const CurrentMapRequestVersion = 11 type StableID string @@ -824,6 +825,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[:]) } 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 +} 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. diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 5af5632df..8b14a5c9e 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) @@ -1056,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. @@ -1412,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 { @@ -1450,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) diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 5c15f3df3..da99a9d6c 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -8,9 +8,11 @@ package monitor import ( + "errors" "sync" "time" + "tailscale.com/net/interfaces" "tailscale.com/types/logger" ) @@ -32,9 +34,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 @@ -46,8 +50,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 @@ -59,17 +64,43 @@ 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) + m := &Mon{ + logf: logf, + cbs: map[*callbackHandle]ChangeFunc{}, + change: make(chan struct{}, 1), + stop: make(chan struct{}), + } + st, err := m.interfaceStateUncached() if err != nil { return nil, err } - return &Mon{ - logf: logf, - cbs: map[*callbackHandle]ChangeFunc{}, - om: om, - change: make(chan struct{}, 1), - stop: make(chan struct{}), - }, nil + 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 +} + +// InterfaceState returns the state of the machine's network interfaces, +// without any Tailscale ones. +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() + } + return s, err } // RegisterChangeCallback adds callback to the set of parties to be @@ -117,17 +148,38 @@ 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: + 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) @@ -137,11 +189,7 @@ func (m *Mon) pump() { if msg.ignore() { continue } - select { - case m.change <- struct{}{}: - case <-m.stop: - return - } + m.InjectEvent() } } @@ -156,16 +204,24 @@ 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: 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 1d7a106a4..1bf3a4d20 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" @@ -24,42 +24,74 @@ 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 } 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[:]) - if err != nil { - return nil, 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 } - 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 +} + +func (m *darwinRouteMon) skipMessage(msg route.Message) bool { + switch msg.(type) { + case *route.InterfaceMulticastAddrMessage: + return true } - if debugRouteMessages { - m.logf("read: %d bytes, %d msgs", n, len(msgs)) - m.logMessages(msgs) - } - return unspecifiedMessage{}, nil + return false } func (m *darwinRouteMon) logMessages(msgs []route.Message) { 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) +} 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_test.go b/wgengine/monitor/monitor_test.go new file mode 100644 index 000000000..9dd012ded --- /dev/null +++ b/wgengine/monitor/monitor_test.go @@ -0,0 +1,89 @@ +// 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 ( + "flag" + "testing" + "time" + + "tailscale.com/net/interfaces" +) + +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) + } +} + +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) { + 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 {} + } +} 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) diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 7fb814151..df03a6bae 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" @@ -31,15 +33,17 @@ 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" "tailscale.com/wgengine" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" "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. @@ -50,12 +54,15 @@ type Impl struct { e wgengine.Engine mc *magicsock.Conn logf logger.Logf + + mu sync.Mutex + 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") } @@ -115,12 +122,45 @@ 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 } +// 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 + ret[strings.TrimRight(nm.Name, ".")] = ip + if dnsname.HasSuffix(nm.Name, suffix) { + ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip + } + } + for _, p := range nm.Peers { + if p.Name != "" && len(p.Addresses) > 0 { + ip := p.Addresses[0].IP + ret[strings.TrimRight(p.Name, ".")] = ip + if dnsname.HasSuffix(p.Name, suffix) { + 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) { + ns.updateDNS(nm) + oldIPs := make(map[tcpip.Address]bool) for _, ip := range ns.ipstack.AllAddresses()[nicID] { oldIPs[ip.AddressWithPrefix.Address] = true @@ -166,10 +206,57 @@ 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 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 + } + 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. + ip := m[host] + 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) { + ns.mu.Lock() + dnsMap := ns.dns + ns.mu.Unlock() + + remoteIPPort, err := dnsMap.Resolve(ctx, addr) + if err != nil { + return nil, err } remoteAddress := tcpip.FullAddress{ NIC: nicID, @@ -201,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 @@ -219,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, @@ -229,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 { @@ -237,19 +331,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) @@ -266,38 +359,27 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, address stri } cancel() }() - server, err := ns.dialContextTCP(ctx, address) + 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 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) -} - -func (ns *Impl) socks5Server() { - ln, err := net.Listen("tcp", "localhost:1080") + err = <-connClosed if err != nil { - ns.logf("could not start SOCKS5 listener: %v", err) - return + ns.logf("proxy connection closed with error: %v", err) } - 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)) + ns.logf("[v2] netstack: forwarder connection on port %v closed", port) } func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) { 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") } 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/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go index 9e7f4be3b..b5d265526 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" ) @@ -50,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) { @@ -248,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 @@ -287,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 @@ -305,7 +339,23 @@ 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.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 + // 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 { + // TODO: do same dummy behavior as v6? return errors.New("Due to a Windows limitation, one cannot have interface routes without an interface address") } @@ -348,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]) }) @@ -374,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 @@ -384,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 } @@ -565,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) } } 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 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 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 { diff --git a/wgengine/userspace.go b/wgengine/userspace.go index d1d447484..8d73e9483 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -120,17 +120,24 @@ 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 // 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 +164,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, }) } @@ -244,8 +233,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,9 +245,12 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ e.linkMon = mon e.linkMonOwned = true } - unregisterMonWatch := e.linkMon.RegisterChangeCallback(func() { - e.LinkChange(false) + + logf("link state: %+v", e.linkMon.InterfaceState()) + + unregisterMonWatch := e.linkMon.RegisterChangeCallback(func(changed bool, st *interfaces.State) { tshttpproxy.InvalidateCache() + e.linkChange(changed, st) }) closePool.addFunc(unregisterMonWatch) e.linkMonUnregister = unregisterMonWatch @@ -286,22 +276,11 @@ 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 { - 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 @@ -1268,30 +1247,21 @@ 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 := getLinkState() - if err != nil { - e.logf("LinkChange: interfaces.GetState: %v", err) - return - } - cur.IsExpensive = isExpensive - needRebind, linkChangeCallback := e.setLinkState(cur) +// 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 { 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") @@ -1300,23 +1270,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() { @@ -1334,14 +1292,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) } 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.go b/wgengine/watchdog.go index 130ce4610..3b96f2e8e 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) }) @@ -101,9 +102,6 @@ func (e *watchdogEngine) 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/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) } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 257d59f26..57bc7cb77 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() @@ -87,15 +90,18 @@ type Engine interface { 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. + // link has changed. + // + // The isExpensive parameter is not used. // // 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. + // 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. @@ -120,13 +126,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