From 7cd4766d5e8bd9cc41b57c87be29aa34f988e0f7 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 7 May 2021 21:38:18 -0700 Subject: [PATCH 1/8] types/wgkey: add BenchmarkShortString Signed-off-by: Josh Bleecher Snyder --- types/wgkey/key_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/types/wgkey/key_test.go b/types/wgkey/key_test.go index 1c4c2da59..c4bfa93da 100644 --- a/types/wgkey/key_test.go +++ b/types/wgkey/key_test.go @@ -171,3 +171,13 @@ func BenchmarkUnmarshalJSON(b *testing.B) { } } } + +var sinkString string + +func BenchmarkShortString(b *testing.B) { + b.ReportAllocs() + var k Key + for i := 0; i < b.N; i++ { + sinkString = k.ShortString() + } +} From e9066ee6257d6c302e130a11703d134069ea5cf3 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 7 May 2021 21:38:31 -0700 Subject: [PATCH 2/8] types/wgkey: optimize Key.ShortString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit name old time/op new time/op delta ShortString-8 82.6ns ± 0% 15.6ns ± 0% -81.07% (p=0.008 n=5+5) name old alloc/op new alloc/op delta ShortString-8 104B ± 0% 8B ± 0% -92.31% (p=0.008 n=5+5) name old allocs/op new allocs/op delta ShortString-8 3.00 ± 0% 1.00 ± 0% -66.67% (p=0.008 n=5+5) Signed-off-by: Josh Bleecher Snyder --- types/wgkey/key.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/types/wgkey/key.go b/types/wgkey/key.go index fe757083a..3e2252010 100644 --- a/types/wgkey/key.go +++ b/types/wgkey/key.go @@ -78,8 +78,16 @@ func (k Key) HexString() string { return hex.EncodeToString(k[:]) } func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 } func (k *Key) ShortString() string { - long := k.Base64() - return "[" + long[0:5] + "]" + // The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]". + // Since we only care about the first 5 characters, it suffices to encode the first 4 bytes of k. + // Encoding those 4 bytes requires 8 bytes. + // Make dst have size 9, to fit the leading '[' plus those 8 bytes. + // We slice the unused ones away at the end. + dst := make([]byte, 9) + dst[0] = '[' + base64.StdEncoding.Encode(dst[1:], k[:4]) + dst[6] = ']' + return string(dst[:7]) } func (k *Key) IsZero() bool { From 6618e82ba2e45150267aa861b30a15f1df48014d Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 7 May 2021 12:53:11 -0700 Subject: [PATCH 3/8] wgengine/bench: close Engines on benchmark completion This reduces the speed with which these benchmarks exhaust their supply fds. Not to zero unfortunately, but it's still helpful when doing long runs. Signed-off-by: Josh Bleecher Snyder --- wgengine/bench/bench.go | 2 +- wgengine/bench/bench_test.go | 2 +- wgengine/bench/wg.go | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/wgengine/bench/bench.go b/wgengine/bench/bench.go index 9c7fdd690..311a99697 100644 --- a/wgengine/bench/bench.go +++ b/wgengine/bench/bench.go @@ -80,7 +80,7 @@ func main() { // tx=134236 rx=133166 (1070 = 0.80% loss) (1088.9 Mbits/sec) case 101: - setupWGTest(logf, traf, Addr1, Addr2) + setupWGTest(nil, logf, traf, Addr1, Addr2) default: log.Fatalf("provide a valid test number (0..n)") diff --git a/wgengine/bench/bench_test.go b/wgengine/bench/bench_test.go index 7f8089445..253209bb6 100644 --- a/wgengine/bench/bench_test.go +++ b/wgengine/bench/bench_test.go @@ -43,7 +43,7 @@ func BenchmarkBatchTCP(b *testing.B) { func BenchmarkWireGuardTest(b *testing.B) { run(b, func(logf logger.Logf, traf *TrafficGen) { - setupWGTest(logf, traf, Addr1, Addr2) + setupWGTest(b, logf, traf, Addr1, Addr2) }) } diff --git a/wgengine/bench/wg.go b/wgengine/bench/wg.go index 167975a3f..e3cb300f4 100644 --- a/wgengine/bench/wg.go +++ b/wgengine/bench/wg.go @@ -10,6 +10,7 @@ "os" "strings" "sync" + "testing" "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" @@ -25,7 +26,7 @@ "tailscale.com/wgengine/wgcfg" ) -func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) { +func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) { l1 := logger.WithPrefix(logf, "e1: ") k1, err := wgkey.NewPrivate() if err != nil { @@ -49,6 +50,9 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) { if err != nil { log.Fatalf("e1 init: %v", err) } + if b != nil { + b.Cleanup(e1.Close) + } l2 := logger.WithPrefix(logf, "e2: ") k2, err := wgkey.NewPrivate() @@ -73,6 +77,9 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) { if err != nil { log.Fatalf("e2 init: %v", err) } + if b != nil { + b.Cleanup(e2.Close) + } e1.SetFilter(filter.NewAllowAllForTest(l1)) e2.SetFilter(filter.NewAllowAllForTest(l2)) From a72fb7ac0bd3d6be68fc019ead356941660333a6 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 7 May 2021 12:55:47 -0700 Subject: [PATCH 4/8] wgengine/bench: handle multiple Engine status callbacks It is possible to get multiple status callbacks from an Engine. We need to wait for at least one from each Engine. Without limiting to one per Engine, wait.Wait can exit early or can panic due to a negative counter. Signed-off-by: Josh Bleecher Snyder --- wgengine/bench/wg.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wgengine/bench/wg.go b/wgengine/bench/wg.go index e3cb300f4..412799e15 100644 --- a/wgengine/bench/wg.go +++ b/wgengine/bench/wg.go @@ -87,6 +87,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netadd var wait sync.WaitGroup wait.Add(2) + var e1waitDoneOnce sync.Once e1.SetStatusCallback(func(st *wgengine.Status, err error) { if err != nil { log.Fatalf("e1 status err: %v", err) @@ -118,9 +119,10 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netadd } c2.Peers = []wgcfg.Peer{p} e2.Reconfig(&c2, &router.Config{}, new(dns.Config)) - wait.Done() + e1waitDoneOnce.Do(wait.Done) }) + var e2waitDoneOnce sync.Once e2.SetStatusCallback(func(st *wgengine.Status, err error) { if err != nil { log.Fatalf("e2 status err: %v", err) @@ -152,7 +154,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netadd } c1.Peers = []wgcfg.Peer{p} e1.Reconfig(&c1, &router.Config{}, new(dns.Config)) - wait.Done() + e2waitDoneOnce.Do(wait.Done) }) // Not using DERP in this test (for now?). From 8d2a90529ef45d83af2926d3ffadca351d8e87b3 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 7 May 2021 12:50:40 -0700 Subject: [PATCH 5/8] wgengine/bench: hold lock in TrafficGen.GotPacket while calling first packet callback Without any synchronization here, the "first packet" callback can be delayed indefinitely, while other work continues. Since the callback starts the benchmark timer, this could skew results. Worse, if the benchmark manages to complete before the benchmark timer begins, it'll cause a data race with the benchmark shutdown performed by package testing. That is what is reported in #1881. This is a bit unfortunate, in that it means that users of TrafficGen have to be careful to keep this callback speedy and lightweight and to avoid deadlocks. Fixes #1881 Signed-off-by: Josh Bleecher Snyder --- wgengine/bench/trafficgen.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wgengine/bench/trafficgen.go b/wgengine/bench/trafficgen.go index 081505440..f2314dc9b 100644 --- a/wgengine/bench/trafficgen.go +++ b/wgengine/bench/trafficgen.go @@ -180,6 +180,7 @@ func (t *TrafficGen) Generate(b []byte, ofs int) int { // GotPacket processes a packet that came back on the receive side. func (t *TrafficGen) GotPacket(b []byte, ofs int) { t.mu.Lock() + defer t.mu.Unlock() s := &t.cur seq := int64(binary.BigEndian.Uint64( @@ -203,9 +204,6 @@ func (t *TrafficGen) GotPacket(b []byte, ofs int) { f := t.onFirstPacket t.onFirstPacket = nil - - t.mu.Unlock() - if f != nil { f() } From 7027fa06c34258857f393393f048e9b67c050daf Mon Sep 17 00:00:00 2001 From: Maisem Ali <3953239+maisem@users.noreply.github.com> Date: Mon, 10 May 2021 09:31:58 -0700 Subject: [PATCH 6/8] wf: implement windows firewall using inet.af/wf. Signed-off-by: Maisem Ali --- cmd/tailscale/cli/cli_test.go | 2 +- go.mod | 7 +- go.sum | 15 +- wf/firewall.go | 510 ++++++++++++++++++++++++++++++++++ 4 files changed, 527 insertions(+), 7 deletions(-) create mode 100644 wf/firewall.go diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index f8372d642..fecdb76b2 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -506,7 +506,7 @@ func TestPrefsFromUpArgs(t *testing.T) { args: upArgsT{ exitNodeIP: "foo", }, - wantErr: `invalid IP address "foo" for --exit-node: unable to parse IP`, + wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`, }, { name: "error_exit_node_allow_lan_without_exit_node", diff --git a/go.mod b/go.mod index 79903628e..9de4ccb22 100644 --- a/go.mod +++ b/go.mod @@ -33,16 +33,17 @@ require ( golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 golang.org/x/net v0.0.0-20210510120150-4163338589ed golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210423082822-04245dca01da + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba - golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 + golang.org/x/tools v0.1.0 golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 gopkg.in/yaml.v2 v2.2.8 // indirect honnef.co/go/tools v0.1.0 - inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 + inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 inet.af/peercred v0.0.0-20210302202138-56e694897155 + inet.af/wf v0.0.0-20210424212123-eaa011a774a4 rsc.io/goversion v1.2.0 ) diff --git a/go.sum b/go.sum index 69d00735c..2f79bbcc3 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/frankban/quicktest v1.12.1 h1:P6vQcHwZYgVGIpUzKB5DXzkEeYJppJOStPLuh9aB89c= @@ -106,6 +107,7 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY= github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk= +github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -197,14 +199,17 @@ golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs= @@ -220,8 +225,9 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -243,11 +249,14 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= -inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4= inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4= +inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d h1:9tuJMxDV7THGfXWirKBD/v9rbsBC21bHd2eEYsYuIek= +inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg= inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM= inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE= inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= +inet.af/wf v0.0.0-20210424212123-eaa011a774a4 h1:g1VVXY1xRKoO17aKY3g9KeJxDW0lGx1n2Y+WPSWkOL8= +inet.af/wf v0.0.0-20210424212123-eaa011a774a4/go.mod h1:56/0QVlZ4NmPRh1QuU2OfrKqjSgt5P39R534gD2JMpQ= rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= diff --git a/wf/firewall.go b/wf/firewall.go new file mode 100644 index 000000000..0eda66e7b --- /dev/null +++ b/wf/firewall.go @@ -0,0 +1,510 @@ +// 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. + +// +build windows + +package wf + +import ( + "fmt" + "os" + + "golang.org/x/sys/windows" + "inet.af/netaddr" + "inet.af/wf" +) + +// Known addresses. +var ( + linkLocalRange = netaddr.MustParseIPPrefix("ff80::/10") + linkLocalDHCPMulticast = netaddr.MustParseIP("ff02::1:2") + siteLocalDHCPMulticast = netaddr.MustParseIP("ff05::1:3") + linkLocalRouterMulticast = netaddr.MustParseIP("ff02::2") +) + +type direction int + +const ( + directionInbound direction = iota + directionOutbound + directionBoth +) + +type protocol int + +const ( + protocolV4 protocol = iota + protocolV6 + protocolAll +) + +// getLayers returns the wf.LayerIDs where the rules should be added based +// on the protocol and direction. +func (p protocol) getLayers(d direction) []wf.LayerID { + var layers []wf.LayerID + if p == protocolAll || p == protocolV4 { + if d == directionBoth || d == directionInbound { + layers = append(layers, wf.LayerALEAuthRecvAcceptV4) + } + if d == directionBoth || d == directionOutbound { + layers = append(layers, wf.LayerALEAuthConnectV4) + } + } + if p == protocolAll || p == protocolV6 { + if d == directionBoth || d == directionInbound { + layers = append(layers, wf.LayerALEAuthRecvAcceptV6) + } + if d == directionBoth || d == directionOutbound { + layers = append(layers, wf.LayerALEAuthConnectV6) + } + } + return layers +} + +func ruleName(action wf.Action, l wf.LayerID, name string) string { + switch l { + case wf.LayerALEAuthConnectV4: + return fmt.Sprintf("%s outbound %s (IPv4)", action, name) + case wf.LayerALEAuthConnectV6: + return fmt.Sprintf("%s outbound %s (IPv6)", action, name) + case wf.LayerALEAuthRecvAcceptV4: + return fmt.Sprintf("%s inbound %s (IPv4)", action, name) + case wf.LayerALEAuthRecvAcceptV6: + return fmt.Sprintf("%s inbound %s (IPv6)", action, name) + } + return "" +} + +// Firewall uses the Windows Filtering Platform to implement a network firewall. +type Firewall struct { + luid uint64 + providerID wf.ProviderID + sublayerID wf.SublayerID + session *wf.Session + + permittedRoutes map[netaddr.IPPrefix][]*wf.Rule +} + +// New returns a new Firewall for the provdied interface ID. +func New(luid uint64) (*Firewall, error) { + session, err := wf.New(&wf.Options{ + Name: "Tailscale firewall", + Dynamic: true, + }) + if err != nil { + return nil, err + } + wguid, err := windows.GenerateGUID() + if err != nil { + return nil, err + } + providerID := wf.ProviderID(wguid) + if err := session.AddProvider(&wf.Provider{ + ID: providerID, + Name: "Tailscale provider", + }); err != nil { + return nil, err + } + wguid, err = windows.GenerateGUID() + if err != nil { + return nil, err + } + sublayerID := wf.SublayerID(wguid) + if err := session.AddSublayer(&wf.Sublayer{ + ID: sublayerID, + Name: "Tailscale permissive and blocking filters", + Weight: 0, + }); err != nil { + return nil, err + } + f := &Firewall{ + luid: luid, + session: session, + providerID: providerID, + sublayerID: sublayerID, + permittedRoutes: make(map[netaddr.IPPrefix][]*wf.Rule), + } + if err := f.enable(); err != nil { + return nil, err + } + return f, nil +} + +type weight uint64 + +const ( + weightTailscaleTraffic weight = 15 + weightKnownTraffic weight = 12 + weightCatchAll weight = 0 +) + +func (f *Firewall) enable() error { + if err := f.permitTailscaleService(weightTailscaleTraffic); err != nil { + return fmt.Errorf("permitTailscaleService failed: %w", err) + } + + if err := f.permitTunInterface(weightTailscaleTraffic); err != nil { + return fmt.Errorf("permitTunInterface failed: %w", err) + } + + if err := f.permitDNS(weightTailscaleTraffic); err != nil { + return fmt.Errorf("permitDNS failed: %w", err) + } + + if err := f.permitLoopback(weightKnownTraffic); err != nil { + return fmt.Errorf("permitLoopback failed: %w", err) + } + + if err := f.permitDHCPv4(weightKnownTraffic); err != nil { + return fmt.Errorf("permitDHCPv4 failed: %w", err) + } + + if err := f.permitDHCPv6(weightKnownTraffic); err != nil { + return fmt.Errorf("permitDHCPv6 failed: %w", err) + } + + if err := f.permitNDP(weightKnownTraffic); err != nil { + return fmt.Errorf("permitNDP failed: %w", 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. + * TODO(maisem): implement this. + err = permitHyperV(session, baseObjects, weightKnownTraffic) + if err != nil { + return wrapErr(err) + } + */ + + if err := f.blockAll(weightCatchAll); err != nil { + return fmt.Errorf("blockAll failed: %w", err) + } + return nil +} + +// UpdatedPermittedRoutes adds rules to allow incoming and outgoing connections +// from the provided prefixes. It will also remove rules for routes that were +// previously added but have been removed. +func (f *Firewall) UpdatePermittedRoutes(newRoutes []netaddr.IPPrefix) error { + var routesToAdd []netaddr.IPPrefix + routeMap := make(map[netaddr.IPPrefix]bool) + for _, r := range newRoutes { + routeMap[r] = true + if _, ok := f.permittedRoutes[r]; !ok { + routesToAdd = append(routesToAdd, r) + } + } + var routesToRemove []netaddr.IPPrefix + for r := range f.permittedRoutes { + if !routeMap[r] { + routesToRemove = append(routesToRemove, r) + } + } + for _, r := range routesToRemove { + for _, rule := range f.permittedRoutes[r] { + if err := f.session.DeleteRule(rule.ID); err != nil { + return err + } + } + delete(f.permittedRoutes, r) + } + for _, r := range routesToAdd { + conditions := []*wf.Match{ + { + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: r, + }, + } + var p protocol + if r.IP.Is4() { + p = protocolV4 + } else { + p = protocolV6 + } + rules, err := f.addRules("local route", weightKnownTraffic, conditions, wf.ActionPermit, p, directionBoth) + if err != nil { + return err + } + f.permittedRoutes[r] = rules + } + return nil +} + +func (f *Firewall) newRule(name string, w weight, layer wf.LayerID, conditions []*wf.Match, action wf.Action) (*wf.Rule, error) { + id, err := windows.GenerateGUID() + if err != nil { + return nil, err + } + return &wf.Rule{ + Name: ruleName(action, layer, name), + ID: wf.RuleID(id), + Provider: f.providerID, + Sublayer: f.sublayerID, + Layer: layer, + Weight: uint64(w), + Conditions: conditions, + Action: action, + }, nil +} + +func (f *Firewall) addRules(name string, w weight, conditions []*wf.Match, action wf.Action, p protocol, d direction) ([]*wf.Rule, error) { + var rules []*wf.Rule + for _, l := range p.getLayers(d) { + r, err := f.newRule(name, w, l, conditions, action) + if err != nil { + return nil, err + } + if err := f.session.AddRule(r); err != nil { + return nil, err + } + rules = append(rules, r) + } + return rules, nil +} + +func (f *Firewall) blockAll(w weight) error { + _, err := f.addRules("all", w, nil, wf.ActionBlock, protocolAll, directionBoth) + return err +} + +func (f *Firewall) permitNDP(w weight) error { + // These are aliased according to: + // https://social.msdn.microsoft.com/Forums/azure/en-US/eb2aa3cd-5f1c-4461-af86-61e7d43ccc23/filtering-icmp-by-type-code?forum=wfp + fieldICMPType := wf.FieldIPLocalPort + fieldICMPCode := wf.FieldIPRemotePort + + var icmpConditions = func(t, c uint16, remoteAddress interface{}) []*wf.Match { + conditions := []*wf.Match{ + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoICMPV6, + }, + { + Field: fieldICMPType, + Op: wf.MatchTypeEqual, + Value: t, + }, + { + Field: fieldICMPCode, + Op: wf.MatchTypeEqual, + Value: c, + }, + } + if remoteAddress != nil { + conditions = append(conditions, &wf.Match{ + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: linkLocalRouterMulticast, + }) + } + return conditions + } + /* 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 + */ + + // + // Router Solicitation Message + // ICMP type 133, code 0. Outgoing. + // + conditions := icmpConditions(133, 0, linkLocalRouterMulticast) + if _, err := f.addRules("NDP type 133", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil { + return err + } + + // + // Router Advertisement Message + // ICMP type 134, code 0. Incoming. + // + conditions = icmpConditions(134, 0, linkLocalRange) + if _, err := f.addRules("NDP type 134", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil { + return err + } + + // + // Neighbor Solicitation Message + // ICMP type 135, code 0. Bi-directional. + // + conditions = icmpConditions(135, 0, nil) + if _, err := f.addRules("NDP type 135", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil { + return err + } + + // + // Neighbor Advertisement Message + // ICMP type 136, code 0. Bi-directional. + // + conditions = icmpConditions(136, 0, nil) + if _, err := f.addRules("NDP type 136", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil { + return err + } + + // + // Redirect Message + // ICMP type 137, code 0. Incoming. + // + conditions = icmpConditions(137, 0, linkLocalRange) + if _, err := f.addRules("NDP type 137", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil { + return err + } + return nil +} + +func (f *Firewall) permitDHCPv6(w weight) error { + var dhcpConditions = func(remoteAddrs ...interface{}) []*wf.Match { + conditions := []*wf.Match{ + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoUDP, + }, + { + Field: wf.FieldIPLocalAddress, + Op: wf.MatchTypeEqual, + Value: linkLocalRange, + }, + { + Field: wf.FieldIPLocalPort, + Op: wf.MatchTypeEqual, + Value: uint16(546), + }, + { + Field: wf.FieldIPRemotePort, + Op: wf.MatchTypeEqual, + Value: uint16(547), + }, + } + for _, a := range remoteAddrs { + conditions = append(conditions, &wf.Match{ + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: a, + }) + } + return conditions + } + conditions := dhcpConditions(linkLocalDHCPMulticast, siteLocalDHCPMulticast) + if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil { + return err + } + conditions = dhcpConditions(linkLocalRange) + if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil { + return err + } + return nil +} + +func (f *Firewall) permitDHCPv4(w weight) error { + var dhcpConditions = func(remoteAddrs ...interface{}) []*wf.Match { + conditions := []*wf.Match{ + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoUDP, + }, + { + Field: wf.FieldIPLocalPort, + Op: wf.MatchTypeEqual, + Value: uint16(68), + }, + { + Field: wf.FieldIPRemotePort, + Op: wf.MatchTypeEqual, + Value: uint16(67), + }, + } + for _, a := range remoteAddrs { + conditions = append(conditions, &wf.Match{ + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: a, + }) + } + return conditions + } + conditions := dhcpConditions(netaddr.IPv4(255, 255, 255, 255)) + if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV4, directionOutbound); err != nil { + return err + } + + conditions = dhcpConditions() + if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV4, directionInbound); err != nil { + return err + } + return nil +} + +func (f *Firewall) permitTunInterface(w weight) error { + condition := []*wf.Match{ + { + Field: wf.FieldIPLocalInterface, + Op: wf.MatchTypeEqual, + Value: f.luid, + }, + } + _, err := f.addRules("on TUN", w, condition, wf.ActionPermit, protocolAll, directionBoth) + return err +} + +func (f *Firewall) permitLoopback(w weight) error { + condition := []*wf.Match{ + { + Field: wf.FieldFlags, + Op: wf.MatchTypeEqual, + Value: wf.ConditionFlagIsLoopback, + }, + } + _, err := f.addRules("on loopback", w, condition, wf.ActionPermit, protocolAll, directionBoth) + return err +} + +func (f *Firewall) permitDNS(w weight) error { + conditions := []*wf.Match{ + { + Field: wf.FieldIPRemotePort, + Op: wf.MatchTypeEqual, + Value: uint16(53), + }, + // Repeat the condition type for logical OR. + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoUDP, + }, + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoTCP, + }, + } + _, err := f.addRules("DNS", w, conditions, wf.ActionPermit, protocolAll, directionBoth) + return err +} + +func (f *Firewall) permitTailscaleService(w weight) error { + currentFile, err := os.Executable() + if err != nil { + return err + } + + appID, err := wf.AppID(currentFile) + if err != nil { + return fmt.Errorf("could not get app id for %q: %w", currentFile, err) + } + conditions := []*wf.Match{ + { + Field: wf.FieldALEAppID, + Op: wf.MatchTypeEqual, + Value: appID, + }, + } + _, err = f.addRules("unrestricted traffic for Tailscale service", w, conditions, wf.ActionPermit, protocolAll, directionBoth) + return err +} From c0a70f3a06ec539b368d76804c54d90709ac7011 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Mon, 10 May 2021 10:59:10 -0700 Subject: [PATCH 7/8] go.mod: pull in wintun alignment fix from upstream wireguard-go https://github.com/tailscale/wireguard-go/compare/6cd106ab1339...030c638da3df Signed-off-by: Josh Bleecher Snyder --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9de4ccb22..e2d9dba7b 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/peterbourgon/ff/v2 v2.0.0 github.com/pkg/errors v0.9.1 // indirect github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 - github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339 + github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df github.com/tcnksm/go-httpstat v0.2.0 github.com/toqueteos/webbrowser v1.2.0 go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 diff --git a/go.sum b/go.sum index 2f79bbcc3..e0a2668e8 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBW github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339 h1:OjLaZ57xeWJUUBAJN5KmsgjsaUABTZhcvgO/lKtZ8sQ= github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c= +github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df h1:ekBw6cxmDhXf9YxTmMZh7SPwUh9rnRRnaoX7HFiGobc= +github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= From dc32b4695c2e195c3380a23d2cd26e0d24004b6b Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 10 May 2021 13:04:32 -0700 Subject: [PATCH 8/8] util/dnsname: normalize leading dots in ToFQDN. Fixes #1888. Signed-off-by: David Anderson --- util/dnsname/dnsname.go | 5 ++++- util/dnsname/dnsname_test.go | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/util/dnsname/dnsname.go b/util/dnsname/dnsname.go index 56ee0baea..905e90f5f 100644 --- a/util/dnsname/dnsname.go +++ b/util/dnsname/dnsname.go @@ -24,13 +24,16 @@ func ToFQDN(s string) (FQDN, error) { if isValidFQDN(s) { return FQDN(s), nil } - if len(s) == 0 { + if len(s) == 0 || s == "." { return FQDN("."), nil } if s[len(s)-1] == '.' { s = s[:len(s)-1] } + if s[0] == '.' { + s = s[1:] + } if len(s) > maxNameLength { return "", fmt.Errorf("%q is too long to be a DNS name", s) } diff --git a/util/dnsname/dnsname_test.go b/util/dnsname/dnsname_test.go index 3df40dded..df3ea30b6 100644 --- a/util/dnsname/dnsname_test.go +++ b/util/dnsname/dnsname_test.go @@ -20,11 +20,12 @@ func TestFQDN(t *testing.T) { {".", ".", false, 0}, {"foo.com", "foo.com.", false, 2}, {"foo.com.", "foo.com.", false, 2}, + {".foo.com.", "foo.com.", false, 2}, + {".foo.com", "foo.com.", false, 2}, {"com", "com.", false, 1}, {"www.tailscale.com", "www.tailscale.com.", false, 3}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com", "", true, 0}, {strings.Repeat("aaaaa.", 60) + "com", "", true, 0}, - {".com", "", true, 0}, {"foo..com", "", true, 0}, }