diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 3e76e25c5..559f214db 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -27,7 +27,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+ šŸ’£ github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+ W šŸ’£ github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+ - github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+ github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli šŸ’£ go4.org/intern from inet.af/netaddr @@ -89,6 +88,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/wgengine/router/dns from tailscale.com/ipn+ tailscale.com/wgengine/tsdns from tailscale.com/ipn+ tailscale.com/wgengine/tstun from tailscale.com/wgengine + tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+ tailscale.com/wgengine/wglog from tailscale.com/wgengine W šŸ’£ tailscale.com/wgengine/winnet from tailscale.com/wgengine/router golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 19bcc78d6..77ef9073c 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -31,7 +31,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+ šŸ’£ github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+ W šŸ’£ github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+ - github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+ github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck šŸ’£ go4.org/intern from inet.af/netaddr šŸ’£ go4.org/mem from tailscale.com/control/controlclient+ @@ -130,6 +129,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/wgengine/router/dns from tailscale.com/ipn+ tailscale.com/wgengine/tsdns from tailscale.com/ipn+ tailscale.com/wgengine/tstun from tailscale.com/wgengine+ + tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+ tailscale.com/wgengine/wglog from tailscale.com/wgengine W šŸ’£ tailscale.com/wgengine/winnet from tailscale.com/wgengine/router golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box diff --git a/control/controlclient/netmap.go b/control/controlclient/netmap.go index c3007c275..40041a491 100644 --- a/control/controlclient/netmap.go +++ b/control/controlclient/netmap.go @@ -13,12 +13,12 @@ "strings" "time" - "github.com/tailscale/wireguard-go/wgcfg" "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/wgkey" "tailscale.com/wgengine/filter" + "tailscale.com/wgengine/wgcfg" ) type NetworkMap struct { diff --git a/go.mod b/go.mod index c306a8d67..9fca67cc8 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 github.com/peterbourgon/ff/v2 v2.0.0 github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 - github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 + github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8 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 6505d1fd5..4ffdb0116 100644 --- a/go.sum +++ b/go.sum @@ -296,6 +296,8 @@ github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4U github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8= github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXul2ysBGZVVM0fKISMgZ9BZRXuOYAyn8MxAbY= github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8= +github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8 h1:7OWHhbjWEuEjt+VlgOXLC4+iPkAvwTMU4zASxa+mKbw= +github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8= 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= diff --git a/internal/deepprint/deepprint_test.go b/internal/deepprint/deepprint_test.go index e2ae45ba7..e5b2b0924 100644 --- a/internal/deepprint/deepprint_test.go +++ b/internal/deepprint/deepprint_test.go @@ -8,10 +8,10 @@ "bytes" "testing" - "github.com/tailscale/wireguard-go/wgcfg" "inet.af/netaddr" "tailscale.com/wgengine/router" "tailscale.com/wgengine/router/dns" + "tailscale.com/wgengine/wgcfg" ) func TestDeepPrint(t *testing.T) { diff --git a/ipn/local.go b/ipn/local.go index 335be2244..0877ad9f7 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -15,7 +15,6 @@ "sync" "time" - "github.com/tailscale/wireguard-go/wgcfg" "golang.org/x/oauth2" "inet.af/netaddr" "tailscale.com/control/controlclient" @@ -37,6 +36,7 @@ "tailscale.com/wgengine/router" "tailscale.com/wgengine/router/dns" "tailscale.com/wgengine/tsdns" + "tailscale.com/wgengine/wgcfg" ) var controlDebugFlags = getControlDebugFlags() diff --git a/wgengine/magicsock/legacy.go b/wgengine/magicsock/legacy.go index 8fe34e5d8..856700925 100644 --- a/wgengine/magicsock/legacy.go +++ b/wgengine/magicsock/legacy.go @@ -19,7 +19,6 @@ "github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/tai64n" - "github.com/tailscale/wireguard-go/wgcfg" "golang.org/x/crypto/blake2s" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" @@ -28,6 +27,7 @@ "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/wgkey" + "tailscale.com/wgengine/wgcfg" ) var ( diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 620394b12..dfa1f6230 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -28,7 +28,6 @@ "github.com/google/go-cmp/cmp" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun/tuntest" - "github.com/tailscale/wireguard-go/wgcfg" "golang.org/x/crypto/nacl/box" "inet.af/netaddr" "tailscale.com/control/controlclient" @@ -46,6 +45,7 @@ "tailscale.com/types/wgkey" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/tstun" + "tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wglog" ) @@ -196,7 +196,7 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der func (s *magicStack) Reconfig(cfg *wgcfg.Config) error { s.wgLogger.SetPeers(cfg.Peers) - return s.dev.Reconfig(cfg) + return wgcfg.ReconfigDevice(s.dev, cfg, s.conn.logf) } func (s *magicStack) String() string { @@ -1131,7 +1131,11 @@ func testTwoDevicePing(t *testing.T, d *devices) { defer setT(outerT) pingSeq(t, 50, 700*time.Millisecond, false) - ep2 := m2.dev.Config().Peers[0].Endpoints + cfg, err := wgcfg.DeviceConfig(m2.dev) + if err != nil { + t.Fatal(err) + } + ep2 := cfg.Peers[0].Endpoints if len(ep2) != 2 { t.Error("handshake spray failed to find real route") } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 5dbfd3f17..8638b3d38 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -23,7 +23,6 @@ "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" - "github.com/tailscale/wireguard-go/wgcfg" "go4.org/mem" "inet.af/netaddr" "tailscale.com/control/controlclient" @@ -46,6 +45,7 @@ "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/tstun" + "tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wglog" ) @@ -836,7 +836,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ } if numRemove > 0 { e.logf("wgengine: Reconfig: removing session keys for %d peers", numRemove) - if err := e.wgdev.Reconfig(&minner); err != nil { + if err := wgcfg.ReconfigDevice(e.wgdev, &minner, e.logf); err != nil { e.logf("wgdev.Reconfig: %v", err) return err } @@ -844,7 +844,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ } e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers)) - if err := e.wgdev.Reconfig(&min); err != nil { + if err := wgcfg.ReconfigDevice(e.wgdev, &min, e.logf); err != nil { e.logf("wgdev.Reconfig: %v", err) return err } diff --git a/wgengine/userspace_test.go b/wgengine/userspace_test.go index eed9ab941..a5cc9d965 100644 --- a/wgengine/userspace_test.go +++ b/wgengine/userspace_test.go @@ -11,13 +11,13 @@ "testing" "time" - "github.com/tailscale/wireguard-go/wgcfg" "go4.org/mem" "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tstun" + "tailscale.com/wgengine/wgcfg" ) func TestNoteReceiveActivity(t *testing.T) { diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index ee0fc3045..91b5fe04e 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -12,7 +12,6 @@ "strings" "time" - "github.com/tailscale/wireguard-go/wgcfg" "inet.af/netaddr" "tailscale.com/control/controlclient" "tailscale.com/ipn/ipnstate" @@ -21,6 +20,7 @@ "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" + "tailscale.com/wgengine/wgcfg" ) // NewWatchdog wraps an Engine and makes sure that all methods complete diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go new file mode 100644 index 000000000..af86b36d6 --- /dev/null +++ b/wgengine/wgcfg/config.go @@ -0,0 +1,67 @@ +// 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 wgcfg has types and a parser for representing WireGuard config. +package wgcfg + +import ( + "inet.af/netaddr" +) + +// Config is a WireGuard configuration. +// It only supports the set of things Tailscale uses. +type Config struct { + Name string + PrivateKey PrivateKey + Addresses []netaddr.IPPrefix + ListenPort uint16 + MTU uint16 + DNS []netaddr.IP + Peers []Peer +} + +type Peer struct { + PublicKey Key + AllowedIPs []netaddr.IPPrefix + Endpoints string // comma-separated host/port pairs: "1.2.3.4:56,[::]:80" + PersistentKeepalive uint16 +} + +// Copy makes a deep copy of Config. +// The result aliases no memory with the original. +func (cfg Config) Copy() Config { + res := cfg + if res.Addresses != nil { + res.Addresses = append([]netaddr.IPPrefix{}, res.Addresses...) + } + if res.DNS != nil { + res.DNS = append([]netaddr.IP{}, res.DNS...) + } + peers := make([]Peer, 0, len(res.Peers)) + for _, peer := range res.Peers { + peers = append(peers, peer.Copy()) + } + res.Peers = peers + return res +} + +// Copy makes a deep copy of Peer. +// The result aliases no memory with the original. +func (peer Peer) Copy() Peer { + res := peer + if res.AllowedIPs != nil { + res.AllowedIPs = append([]netaddr.IPPrefix{}, res.AllowedIPs...) + } + return res +} + +// PeerWithKey returns the Peer with key k and reports whether it was found. +func (config Config) PeerWithKey(k Key) (Peer, bool) { + for _, p := range config.Peers { + if p.PublicKey == k { + return p, true + } + } + return Peer{}, false +} diff --git a/wgengine/wgcfg/device.go b/wgengine/wgcfg/device.go new file mode 100644 index 000000000..fd00f2229 --- /dev/null +++ b/wgengine/wgcfg/device.go @@ -0,0 +1,61 @@ +// 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 wgcfg + +import ( + "io" + "sort" + + "github.com/tailscale/wireguard-go/device" + "tailscale.com/types/logger" +) + +func DeviceConfig(d *device.Device) (*Config, error) { + r, w := io.Pipe() + errc := make(chan error, 1) + go func() { + errc <- d.IpcGetOperation(w) + w.Close() + }() + cfg, err := FromUAPI(r) + if err != nil { + return nil, err + } + if err := <-errc; err != nil { + return nil, err + } + + sort.Slice(cfg.Peers, func(i, j int) bool { + return cfg.Peers[i].PublicKey.LessThan(&cfg.Peers[j].PublicKey) + }) + return cfg, nil +} + +// ReconfigDevice replaces the existing device configuration with cfg. +func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) { + defer func() { + if err != nil { + logf("wgcfg.Reconfig failed: %v", err) + } + }() + + prev, err := DeviceConfig(d) + if err != nil { + return err + } + + r, w := io.Pipe() + errc := make(chan error) + go func() { + errc <- d.IpcSetOperation(r) + }() + + err = cfg.ToUAPI(w, prev) + if err != nil { + return err + } + w.Close() + return <-errc +} diff --git a/wgengine/wgcfg/device_test.go b/wgengine/wgcfg/device_test.go new file mode 100644 index 000000000..d48da7c52 --- /dev/null +++ b/wgengine/wgcfg/device_test.go @@ -0,0 +1,242 @@ +// 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 wgcfg + +import ( + "bufio" + "bytes" + "io" + "os" + "sort" + "strings" + "sync" + "testing" + + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/tun" + "inet.af/netaddr" + "tailscale.com/types/wgkey" +) + +func TestDeviceConfig(t *testing.T) { + newPrivateKey := func() (Key, PrivateKey) { + t.Helper() + pk, err := wgkey.NewPrivate() + if err != nil { + t.Fatal(err) + } + return Key(pk.Public()), PrivateKey(pk) + } + k1, pk1 := newPrivateKey() + ip1 := netaddr.MustParseIPPrefix("10.0.0.1/32") + + k2, pk2 := newPrivateKey() + ip2 := netaddr.MustParseIPPrefix("10.0.0.2/32") + + k3, _ := newPrivateKey() + ip3 := netaddr.MustParseIPPrefix("10.0.0.3/32") + + cfg1 := &Config{ + PrivateKey: PrivateKey(pk1), + Peers: []Peer{{ + PublicKey: k2, + AllowedIPs: []netaddr.IPPrefix{ip2}, + }}, + } + + cfg2 := &Config{ + PrivateKey: PrivateKey(pk2), + Peers: []Peer{{ + PublicKey: k1, + AllowedIPs: []netaddr.IPPrefix{ip1}, + PersistentKeepalive: 5, + }}, + } + + device1 := device.NewDevice(newNilTun(), &device.DeviceOptions{ + Logger: device.NewLogger(device.LogLevelError, "device1"), + }) + device2 := device.NewDevice(newNilTun(), &device.DeviceOptions{ + Logger: device.NewLogger(device.LogLevelError, "device2"), + }) + defer device1.Close() + defer device2.Close() + + cmp := func(t *testing.T, d *device.Device, want *Config) { + t.Helper() + got, err := DeviceConfig(d) + if err != nil { + t.Fatal(err) + } + prev := new(Config) + gotbuf := new(strings.Builder) + err = got.ToUAPI(gotbuf, prev) + gotStr := gotbuf.String() + if err != nil { + t.Errorf("got.ToUAPI(): error: %v", err) + return + } + wantbuf := new(strings.Builder) + err = want.ToUAPI(wantbuf, prev) + wantStr := wantbuf.String() + if err != nil { + t.Errorf("want.ToUAPI(): error: %v", err) + return + } + if gotStr != wantStr { + buf := new(bytes.Buffer) + w := bufio.NewWriter(buf) + if err := d.IpcGetOperation(w); err != nil { + t.Errorf("on error, could not IpcGetOperation: %v", err) + } + w.Flush() + t.Errorf("cfg:\n%s\n---- want:\n%s\n---- uapi:\n%s", gotStr, wantStr, buf.String()) + } + } + + t.Run("device1 config", func(t *testing.T) { + if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil { + t.Fatal(err) + } + cmp(t, device1, cfg1) + }) + + t.Run("device2 config", func(t *testing.T) { + if err := ReconfigDevice(device2, cfg2, t.Logf); err != nil { + t.Fatal(err) + } + cmp(t, device2, cfg2) + }) + + // This is only to test that Config and Reconfig are properly synchronized. + t.Run("device2 config/reconfig", func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(2) + + go func() { + ReconfigDevice(device2, cfg2, t.Logf) + wg.Done() + }() + + go func() { + DeviceConfig(device2) + wg.Done() + }() + + wg.Wait() + }) + + t.Run("device1 modify peer", func(t *testing.T) { + cfg1.Peers[0].Endpoints = "1.2.3.4:12345" + if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil { + t.Fatal(err) + } + cmp(t, device1, cfg1) + }) + + t.Run("device1 replace endpoint", func(t *testing.T) { + cfg1.Peers[0].Endpoints = "1.1.1.1:123" + if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil { + t.Fatal(err) + } + cmp(t, device1, cfg1) + }) + + t.Run("device1 add new peer", func(t *testing.T) { + cfg1.Peers = append(cfg1.Peers, Peer{ + PublicKey: k3, + AllowedIPs: []netaddr.IPPrefix{ip3}, + }) + sort.Slice(cfg1.Peers, func(i, j int) bool { + return cfg1.Peers[i].PublicKey.LessThan(&cfg1.Peers[j].PublicKey) + }) + + origCfg, err := DeviceConfig(device1) + if err != nil { + t.Fatal(err) + } + + if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil { + t.Fatal(err) + } + cmp(t, device1, cfg1) + + newCfg, err := DeviceConfig(device1) + if err != nil { + t.Fatal(err) + } + + peer0 := func(cfg *Config) Peer { + p, ok := cfg.PeerWithKey(k2) + if !ok { + t.Helper() + t.Fatal("failed to look up peer 2") + } + return p + } + peersEqual := func(p, q Peer) bool { + return p.PublicKey == q.PublicKey && p.PersistentKeepalive == q.PersistentKeepalive && + p.Endpoints == q.Endpoints && cidrsEqual(p.AllowedIPs, q.AllowedIPs) + } + if !peersEqual(peer0(origCfg), peer0(newCfg)) { + t.Error("reconfig modified old peer") + } + }) + + t.Run("device1 remove peer", func(t *testing.T) { + removeKey := cfg1.Peers[len(cfg1.Peers)-1].PublicKey + cfg1.Peers = cfg1.Peers[:len(cfg1.Peers)-1] + + if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil { + t.Fatal(err) + } + cmp(t, device1, cfg1) + + newCfg, err := DeviceConfig(device1) + if err != nil { + t.Fatal(err) + } + + _, ok := newCfg.PeerWithKey(removeKey) + if ok { + t.Error("reconfig failed to remove peer") + } + }) +} + +// TODO: replace with a loopback tunnel +type nilTun struct { + events chan tun.Event + closed chan struct{} +} + +func newNilTun() tun.Device { + return &nilTun{ + events: make(chan tun.Event), + closed: make(chan struct{}), + } +} + +func (t *nilTun) File() *os.File { return nil } +func (t *nilTun) Flush() error { return nil } +func (t *nilTun) MTU() (int, error) { return 1420, nil } +func (t *nilTun) Name() (string, error) { return "niltun", nil } +func (t *nilTun) Events() chan tun.Event { return t.events } + +func (t *nilTun) Read(data []byte, offset int) (int, error) { + <-t.closed + return 0, io.EOF +} + +func (t *nilTun) Write(data []byte, offset int) (int, error) { + <-t.closed + return 0, io.EOF +} + +func (t *nilTun) Close() error { + close(t.events) + close(t.closed) + return nil +} diff --git a/wgengine/wgcfg/key.go b/wgengine/wgcfg/key.go new file mode 100644 index 000000000..48601df98 --- /dev/null +++ b/wgengine/wgcfg/key.go @@ -0,0 +1,240 @@ +// 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 wgcfg + +import ( + "bytes" + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strings" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" +) + +const KeySize = 32 + +// Key is curve25519 key. +// It is used by WireGuard to represent public and preshared keys. +type Key [KeySize]byte + +// NewPresharedKey generates a new random key. +func NewPresharedKey() (*Key, error) { + var k [KeySize]byte + _, err := rand.Read(k[:]) + if err != nil { + return nil, err + } + return (*Key)(&k), nil +} + +func ParseKey(b64 string) (*Key, error) { return parseKeyBase64(base64.StdEncoding, b64) } + +func ParseHexKey(s string) (Key, error) { + b, err := hex.DecodeString(s) + if err != nil { + return Key{}, &ParseError{"invalid hex key: " + err.Error(), s} + } + if len(b) != KeySize { + return Key{}, &ParseError{fmt.Sprintf("invalid hex key length: %d", len(b)), s} + } + + var key Key + copy(key[:], b) + return key, nil +} + +func ParsePrivateHexKey(v string) (PrivateKey, error) { + k, err := ParseHexKey(v) + if err != nil { + return PrivateKey{}, err + } + pk := PrivateKey(k) + if pk.IsZero() { + // Do not clamp a zero key, pass the zero through + // (much like NaN propagation) so that IsZero reports + // a useful result. + return pk, nil + } + pk.clamp() + return pk, nil +} + +func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) } +func (k Key) String() string { return k.ShortString() } +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] + "]" +} + +func (k *Key) IsZero() bool { + if k == nil { + return true + } + var zeros Key + return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1 +} + +func (k *Key) MarshalJSON() ([]byte, error) { + if k == nil { + return []byte("null"), nil + } + buf := new(bytes.Buffer) + fmt.Fprintf(buf, `"%x"`, k[:]) + return buf.Bytes(), nil +} + +func (k *Key) UnmarshalJSON(b []byte) error { + if k == nil { + return errors.New("wgcfg.Key: UnmarshalJSON on nil pointer") + } + if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' { + return errors.New("wgcfg.Key: UnmarshalJSON not given a string") + } + b = b[1 : len(b)-1] + key, err := ParseHexKey(string(b)) + if err != nil { + return fmt.Errorf("wgcfg.Key: UnmarshalJSON: %v", err) + } + copy(k[:], key[:]) + return nil +} + +func (a *Key) LessThan(b *Key) bool { + for i := range a { + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } + return false +} + +// PrivateKey is curve25519 key. +// It is used by WireGuard to represent private keys. +type PrivateKey [KeySize]byte + +// NewPrivateKey generates a new curve25519 secret key. +// It conforms to the format described on https://cr.yp.to/ecdh.html. +func NewPrivateKey() (PrivateKey, error) { + k, err := NewPresharedKey() + if err != nil { + return PrivateKey{}, err + } + k[0] &= 248 + k[31] = (k[31] & 127) | 64 + return (PrivateKey)(*k), nil +} + +func ParsePrivateKey(b64 string) (*PrivateKey, error) { + k, err := parseKeyBase64(base64.StdEncoding, b64) + return (*PrivateKey)(k), err +} + +func (k *PrivateKey) String() string { return base64.StdEncoding.EncodeToString(k[:]) } +func (k *PrivateKey) HexString() string { return hex.EncodeToString(k[:]) } +func (k *PrivateKey) Equal(k2 PrivateKey) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 } + +func (k *PrivateKey) IsZero() bool { + pk := Key(*k) + return pk.IsZero() +} + +func (k *PrivateKey) clamp() { + k[0] &= 248 + k[31] = (k[31] & 127) | 64 +} + +// Public computes the public key matching this curve25519 secret key. +func (k *PrivateKey) Public() Key { + pk := Key(*k) + if pk.IsZero() { + panic("Tried to generate emptyPrivateKey.Public()") + } + var p [KeySize]byte + curve25519.ScalarBaseMult(&p, (*[KeySize]byte)(k)) + return (Key)(p) +} + +func (k PrivateKey) MarshalText() ([]byte, error) { + buf := new(bytes.Buffer) + fmt.Fprintf(buf, `privkey:%x`, k[:]) + return buf.Bytes(), nil +} + +func (k *PrivateKey) UnmarshalText(b []byte) error { + s := string(b) + if !strings.HasPrefix(s, `privkey:`) { + return errors.New("wgcfg.PrivateKey: UnmarshalText not given a private-key string") + } + s = strings.TrimPrefix(s, `privkey:`) + key, err := ParseHexKey(s) + if err != nil { + return fmt.Errorf("wgcfg.PrivateKey: UnmarshalText: %v", err) + } + copy(k[:], key[:]) + return nil +} + +func (k PrivateKey) SharedSecret(pub Key) (ss [KeySize]byte) { + apk := (*[KeySize]byte)(&pub) + ask := (*[KeySize]byte)(&k) + curve25519.ScalarMult(&ss, ask, apk) //lint:ignore SA1019 Jason says this is OK; match wireguard-go exactyl + return ss +} + +func parseKeyBase64(enc *base64.Encoding, s string) (*Key, error) { + k, err := enc.DecodeString(s) + if err != nil { + return nil, &ParseError{"Invalid key: " + err.Error(), s} + } + if len(k) != KeySize { + return nil, &ParseError{"Keys must decode to exactly 32 bytes", s} + } + var key Key + copy(key[:], k) + return &key, nil +} + +func ParseSymmetricKey(b64 string) (SymmetricKey, error) { + k, err := parseKeyBase64(base64.StdEncoding, b64) + if err != nil { + return SymmetricKey{}, err + } + return SymmetricKey(*k), nil +} + +func ParseSymmetricHexKey(s string) (SymmetricKey, error) { + b, err := hex.DecodeString(s) + if err != nil { + return SymmetricKey{}, &ParseError{"invalid symmetric hex key: " + err.Error(), s} + } + if len(b) != chacha20poly1305.KeySize { + return SymmetricKey{}, &ParseError{fmt.Sprintf("invalid symmetric hex key length: %d", len(b)), s} + } + var key SymmetricKey + copy(key[:], b) + return key, nil +} + +// SymmetricKey is a chacha20poly1305 key. +// It is used by WireGuard to represent pre-shared symmetric keys. +type SymmetricKey [chacha20poly1305.KeySize]byte + +func (k SymmetricKey) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) } +func (k SymmetricKey) String() string { return "sym:" + k.Base64()[:8] } +func (k SymmetricKey) HexString() string { return hex.EncodeToString(k[:]) } +func (k SymmetricKey) IsZero() bool { return k.Equal(SymmetricKey{}) } +func (k SymmetricKey) Equal(k2 SymmetricKey) bool { + return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 +} diff --git a/wgengine/wgcfg/key_test.go b/wgengine/wgcfg/key_test.go new file mode 100644 index 000000000..709b1afcc --- /dev/null +++ b/wgengine/wgcfg/key_test.go @@ -0,0 +1,111 @@ +// 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 wgcfg + +import ( + "bytes" + "testing" +) + +func TestKeyBasics(t *testing.T) { + k1, err := NewPresharedKey() + if err != nil { + t.Fatal(err) + } + + b, err := k1.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + t.Run("JSON round-trip", func(t *testing.T) { + // should preserve the keys + k2 := new(Key) + if err := k2.UnmarshalJSON(b); err != nil { + t.Fatal(err) + } + if !bytes.Equal(k1[:], k2[:]) { + t.Fatalf("k1 %v != k2 %v", k1[:], k2[:]) + } + if b1, b2 := k1.String(), k2.String(); b1 != b2 { + t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2) + } + }) + + t.Run("JSON incompatible with PrivateKey", func(t *testing.T) { + k2 := new(PrivateKey) + if err := k2.UnmarshalText(b); err == nil { + t.Fatalf("successfully decoded key as private key") + } + }) + + t.Run("second key", func(t *testing.T) { + // A second call to NewPresharedKey should make a new key. + k3, err := NewPresharedKey() + if err != nil { + t.Fatal(err) + } + if bytes.Equal(k1[:], k3[:]) { + t.Fatalf("k1 %v == k3 %v", k1[:], k3[:]) + } + // Check for obvious comparables to make sure we are not generating bad strings somewhere. + if b1, b2 := k1.String(), k3.String(); b1 == b2 { + t.Fatalf("base64-encoded keys match: %s, %s", b1, b2) + } + }) +} +func TestPrivateKeyBasics(t *testing.T) { + pri, err := NewPrivateKey() + if err != nil { + t.Fatal(err) + } + + b, err := pri.MarshalText() + if err != nil { + t.Fatal(err) + } + + t.Run("JSON round-trip", func(t *testing.T) { + // should preserve the keys + pri2 := new(PrivateKey) + if err := pri2.UnmarshalText(b); err != nil { + t.Fatal(err) + } + if !bytes.Equal(pri[:], pri2[:]) { + t.Fatalf("pri %v != pri2 %v", pri[:], pri2[:]) + } + if b1, b2 := pri.String(), pri2.String(); b1 != b2 { + t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2) + } + if pub1, pub2 := pri.Public().String(), pri2.Public().String(); pub1 != pub2 { + t.Fatalf("base64-encoded public keys do not match: %s, %s", pub1, pub2) + } + }) + + t.Run("JSON incompatible with Key", func(t *testing.T) { + k2 := new(Key) + if err := k2.UnmarshalJSON(b); err == nil { + t.Fatalf("successfully decoded private key as key") + } + }) + + t.Run("second key", func(t *testing.T) { + // A second call to New should make a new key. + pri3, err := NewPrivateKey() + if err != nil { + t.Fatal(err) + } + if bytes.Equal(pri[:], pri3[:]) { + t.Fatalf("pri %v == pri3 %v", pri[:], pri3[:]) + } + // Check for obvious comparables to make sure we are not generating bad strings somewhere. + if b1, b2 := pri.String(), pri3.String(); b1 == b2 { + t.Fatalf("base64-encoded keys match: %s, %s", b1, b2) + } + if pub1, pub2 := pri.Public().String(), pri3.Public().String(); pub1 == pub2 { + t.Fatalf("base64-encoded public keys match: %s, %s", pub1, pub2) + } + }) +} diff --git a/wgengine/wgcfg/parser.go b/wgengine/wgcfg/parser.go new file mode 100644 index 000000000..518a93992 --- /dev/null +++ b/wgengine/wgcfg/parser.go @@ -0,0 +1,197 @@ +// 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 wgcfg + +import ( + "bufio" + "encoding/hex" + "fmt" + "io" + "net" + "strconv" + "strings" + + "inet.af/netaddr" +) + +type ParseError struct { + why string + offender string +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("%s: ā€˜%sā€™", e.why, e.offender) +} + +func validateEndpoints(s string) error { + vals := strings.Split(s, ",") + for _, val := range vals { + _, _, err := parseEndpoint(val) + if err != nil { + return err + } + } + return nil +} + +func parseEndpoint(s string) (host string, port uint16, err error) { + i := strings.LastIndexByte(s, ':') + if i < 0 { + return "", 0, &ParseError{"Missing port from endpoint", s} + } + host, portStr := s[:i], s[i+1:] + if len(host) < 1 { + return "", 0, &ParseError{"Invalid endpoint host", host} + } + uport, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return "", 0, err + } + hostColon := strings.IndexByte(host, ':') + if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 { + err := &ParseError{"Brackets must contain an IPv6 address", host} + if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 { + maybeV6 := net.ParseIP(host[1 : len(host)-1]) + if maybeV6 == nil || len(maybeV6) != net.IPv6len { + return "", 0, err + } + } else { + return "", 0, err + } + host = host[1 : len(host)-1] + } + return host, uint16(uport), nil +} + +func parseKeyHex(s string) (*Key, error) { + k, err := hex.DecodeString(s) + if err != nil { + return nil, &ParseError{"Invalid key: " + err.Error(), s} + } + if len(k) != KeySize { + return nil, &ParseError{"Keys must decode to exactly 32 bytes", s} + } + var key Key + copy(key[:], k) + return &key, nil +} + +// FromUAPI generates a Config from r. +// r should be generated by calling device.IpcGetOperation; +// it is not compatible with other uapi streams. +func FromUAPI(r io.Reader) (*Config, error) { + cfg := new(Config) + var peer *Peer // current peer being operated on + deviceConfig := true + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + parts := strings.Split(line, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("failed to parse line %q, found %d =-separated parts, want 2", line, len(parts)) + } + key := parts[0] + value := parts[1] + + if key == "public_key" { + if deviceConfig { + deviceConfig = false + } + // Load/create the peer we are now configuring. + var err error + peer, err = cfg.handlePublicKeyLine(value) + if err != nil { + return nil, err + } + continue + } + + var err error + if deviceConfig { + err = cfg.handleDeviceLine(key, value) + } else { + err = cfg.handlePeerLine(peer, key, value) + } + if err != nil { + return nil, err + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return cfg, nil +} + +func (cfg *Config) handleDeviceLine(key, value string) error { + switch key { + case "private_key": + k, err := parseKeyHex(value) + if err != nil { + return err + } + // wireguard-go guarantees not to send zero value; private keys are already clamped. + cfg.PrivateKey = PrivateKey(*k) + case "listen_port": + port, err := strconv.ParseUint(value, 10, 16) + if err != nil { + return fmt.Errorf("failed to parse listen_port: %w", err) + } + cfg.ListenPort = uint16(port) + case "fwmark": + // ignore + default: + return fmt.Errorf("unexpected IpcGetOperation key: %v", key) + } + return nil +} + +func (cfg *Config) handlePublicKeyLine(value string) (*Peer, error) { + k, err := parseKeyHex(value) + if err != nil { + return nil, err + } + cfg.Peers = append(cfg.Peers, Peer{}) + peer := &cfg.Peers[len(cfg.Peers)-1] + peer.PublicKey = *k + return peer, nil +} + +func (cfg *Config) handlePeerLine(peer *Peer, key, value string) error { + switch key { + case "endpoint": + err := validateEndpoints(value) + if err != nil { + return err + } + peer.Endpoints = value + case "persistent_keepalive_interval": + n, err := strconv.ParseUint(value, 10, 16) + if err != nil { + return err + } + peer.PersistentKeepalive = uint16(n) + case "allowed_ip": + ipp, err := netaddr.ParseIPPrefix(value) + if err != nil { + return err + } + peer.AllowedIPs = append(peer.AllowedIPs, ipp) + case "protocol_version": + if value != "1" { + return fmt.Errorf("invalid protocol version: %v", value) + } + case "preshared_key", "last_handshake_time_sec", "last_handshake_time_nsec", "tx_bytes", "rx_bytes": + // ignore + default: + return fmt.Errorf("unexpected IpcGetOperation key: %v", key) + } + return nil +} diff --git a/wgengine/wgcfg/parser_test.go b/wgengine/wgcfg/parser_test.go new file mode 100644 index 000000000..e101a3a05 --- /dev/null +++ b/wgengine/wgcfg/parser_test.go @@ -0,0 +1,55 @@ +// 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 wgcfg + +import ( + "reflect" + "runtime" + "testing" +) + +func noError(t *testing.T, err error) bool { + if err == nil { + return true + } + _, fn, line, _ := runtime.Caller(1) + t.Errorf("Error at %s:%d: %#v", fn, line, err) + return false +} + +func equal(t *testing.T, expected, actual interface{}) bool { + if reflect.DeepEqual(expected, actual) { + return true + } + _, fn, line, _ := runtime.Caller(1) + t.Errorf("Failed equals at %s:%d\nactual %#v\nexpected %#v", fn, line, actual, expected) + return false +} + +func TestParseEndpoint(t *testing.T) { + _, _, err := parseEndpoint("[192.168.42.0:]:51880") + if err == nil { + t.Error("Error was expected") + } + host, port, err := parseEndpoint("192.168.42.0:51880") + if noError(t, err) { + equal(t, "192.168.42.0", host) + equal(t, uint16(51880), port) + } + host, port, err = parseEndpoint("test.wireguard.com:18981") + if noError(t, err) { + equal(t, "test.wireguard.com", host) + equal(t, uint16(18981), port) + } + host, port, err = parseEndpoint("[2607:5300:60:6b0::c05f:543]:2468") + if noError(t, err) { + equal(t, "2607:5300:60:6b0::c05f:543", host) + equal(t, uint16(2468), port) + } + _, _, err = parseEndpoint("[::::::invalid:18981") + if err == nil { + t.Error("Error was expected") + } +} diff --git a/wgengine/wgcfg/writer.go b/wgengine/wgcfg/writer.go new file mode 100644 index 000000000..079c1eb5e --- /dev/null +++ b/wgengine/wgcfg/writer.go @@ -0,0 +1,141 @@ +// 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 wgcfg + +import ( + "fmt" + "io" + "sort" + "strconv" + "strings" + + "inet.af/netaddr" +) + +// ToUAPI writes cfg in UAPI format to w. +// Prev is the previous device Config. +// Prev is required so that we can remove now-defunct peers +// without having to remove and re-add all peers. +func (cfg *Config) ToUAPI(w io.Writer, prev *Config) error { + var stickyErr error + set := func(key, value string) { + if stickyErr != nil { + return + } + _, err := fmt.Fprintf(w, "%s=%s\n", key, value) + if err != nil { + stickyErr = err + } + } + setUint16 := func(key string, value uint16) { + set(key, strconv.FormatUint(uint64(value), 10)) + } + setPeer := func(peer Peer) { + set("public_key", peer.PublicKey.HexString()) + } + + // Device config. + if prev.PrivateKey != cfg.PrivateKey { + set("private_key", cfg.PrivateKey.HexString()) + } + if prev.ListenPort != cfg.ListenPort { + setUint16("listen_port", cfg.ListenPort) + } + + old := make(map[Key]Peer) + for _, p := range prev.Peers { + old[p.PublicKey] = p + } + + // Add/configure all new peers. + for _, p := range cfg.Peers { + oldPeer := old[p.PublicKey] + setPeer(p) + set("protocol_version", "1") + + if !endpointsEqual(oldPeer.Endpoints, p.Endpoints) { + set("endpoint", p.Endpoints) + } + + // TODO: replace_allowed_ips is expensive. + // If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs, + // then skip replace_allowed_ips and instead add only + // the new ipps with allowed_ip. + if !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) { + set("replace_allowed_ips", "true") + for _, ipp := range p.AllowedIPs { + set("allowed_ip", ipp.String()) + } + } + + // Set PersistentKeepalive after the peer is otherwise configured, + // because it can trigger handshake packets. + if oldPeer.PersistentKeepalive != p.PersistentKeepalive { + setUint16("persistent_keepalive_interval", p.PersistentKeepalive) + } + } + + // Remove peers that were present but should no longer be. + for _, p := range cfg.Peers { + delete(old, p.PublicKey) + } + for _, p := range old { + setPeer(p) + set("remove", "true") + } + + if stickyErr != nil { + stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr) + } + return stickyErr +} + +func endpointsEqual(x, y string) bool { + // Cheap comparisons. + if x == y { + return true + } + xs := strings.Split(x, ",") + ys := strings.Split(y, ",") + if len(xs) != len(ys) { + return false + } + // Otherwise, see if they're the same, but out of order. + sort.Strings(xs) + sort.Strings(ys) + x = strings.Join(xs, ",") + y = strings.Join(ys, ",") + return x == y +} + +func cidrsEqual(x, y []netaddr.IPPrefix) bool { + // TODO: re-implement using netaddr.IPSet.Equal. + if len(x) != len(y) { + return false + } + // First see if they're equal in order, without allocating. + exact := true + for i := range x { + if x[i] != y[i] { + exact = false + break + } + } + if exact { + return true + } + + // Otherwise, see if they're the same, but out of order. + m := make(map[netaddr.IPPrefix]bool) + for _, v := range x { + m[v] = true + } + for _, v := range y { + if !m[v] { + return false + } + } + return true +} diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 3139dc1f2..563888083 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -8,7 +8,6 @@ "errors" "time" - "github.com/tailscale/wireguard-go/wgcfg" "inet.af/netaddr" "tailscale.com/control/controlclient" "tailscale.com/ipn/ipnstate" @@ -17,6 +16,7 @@ "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" + "tailscale.com/wgengine/wgcfg" ) // ByteCount is the number of bytes that have been sent or received. diff --git a/wgengine/wglog/wglog.go b/wgengine/wglog/wglog.go index e833bb27a..6f3e490f2 100644 --- a/wgengine/wglog/wglog.go +++ b/wgengine/wglog/wglog.go @@ -12,8 +12,8 @@ "sync/atomic" "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/wgcfg" "tailscale.com/types/logger" + "tailscale.com/wgengine/wgcfg" ) // A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines. diff --git a/wgengine/wglog/wglog_test.go b/wgengine/wglog/wglog_test.go index 3a899839e..0b93a130a 100644 --- a/wgengine/wglog/wglog_test.go +++ b/wgengine/wglog/wglog_test.go @@ -8,7 +8,7 @@ "fmt" "testing" - "github.com/tailscale/wireguard-go/wgcfg" + "tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wglog" )