diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 756998719..8b31f8115 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -28,7 +28,22 @@ jobs: - name: Basic build run: go build ./cmd/... - - name: Test build + - name: macOS build + env: + GOOS: darwin + GOARCH: amd64 + run: go build ./cmd/... + + - name: Windows build + env: + GOOS: windows + GOARCH: amd64 + run: go build ./cmd/... + + - name: Cross-compile tests for other geese + run: ./test.sh + + - name: Run tests on linux run: go test ./... - uses: k0kubun/action-slack@v2.0.0 @@ -45,3 +60,4 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} if: failure() && github.event_name == 'push' + diff --git a/cmd/tailscaled/ipnd.go b/cmd/tailscaled/ipnd.go index 466c074b2..8d2b68b77 100644 --- a/cmd/tailscaled/ipnd.go +++ b/cmd/tailscaled/ipnd.go @@ -20,11 +20,14 @@ import ( "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" "tailscale.com/wgengine" + "tailscale.com/wgengine/magicsock" ) func main() { fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap") debug := getopt.StringLong("debug", 0, "", "Address of debug server") + tunname := getopt.StringLong("tun", 0, "ts0", "tunnel interface name") + listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)") logf := wgengine.RusagePrefixLog(log.Printf) @@ -47,7 +50,7 @@ func main() { if *fake { e, err = wgengine.NewFakeUserspaceEngine(logf, 0, false) } else { - e, err = wgengine.NewUserspaceEngine(logf, "ts0", 0, false) + e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport, false) } if err != nil { log.Fatalf("wgengine.New: %v\n", err) diff --git a/cmd/tsshd/tsshd.go b/cmd/tsshd/tsshd.go new file mode 100644 index 000000000..b96df803a --- /dev/null +++ b/cmd/tsshd/tsshd.go @@ -0,0 +1,205 @@ +// 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 !windows + +// The tsshd binary is an SSH server that accepts connections +// from anybody on the same Tailscale network. +// +// It does not use passwords or SSH public key. +// +// Any user name is accepted; users are logged in as whoever is +// running this daemon. +// +// Warning: use at your own risk. This code has had very few eyeballs +// on it. +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "strings" + "syscall" + "time" + "unsafe" + + "github.com/gliderlabs/ssh" + "github.com/kr/pty" + gossh "golang.org/x/crypto/ssh" +) + +var ( + port = flag.Int("port", 2200, "port to listen on") + hostKey = flag.String("hostkey", "", "SSH host key") +) + +func main() { + flag.Parse() + if *hostKey == "" { + log.Fatalf("missing required --hostkey") + } + hostKey, err := ioutil.ReadFile(*hostKey) + if err != nil { + log.Fatal(err) + } + signer, err := gossh.ParsePrivateKey(hostKey) + if err != nil { + log.Printf("failed to parse SSH host key: %v", err) + return + } + + warned := false + for { + addr, iface, err := tailscaleInterface() + if err != nil { + log.Fatalf("listing interfaces: %v", err) + } + if addr == nil { + if !warned { + log.Printf("no tailscale interface found; polling until one is available") + warned = true + } + // TODO: use netlink or other OS-specific mechanism to efficiently + // wait for change in interfaces. Polling every N seconds is good enough + // for now. + time.Sleep(5 * time.Second) + continue + } + warned = false + listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port)) + log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen) + s := &ssh.Server{ + Addr: listen, + Handler: handleSSH, + } + s.AddHostKey(signer) + + err = s.ListenAndServe() + log.Fatalf("tailscale sshd failed: %v", err) + } + +} + +// tailscaleInterface returns an err on a fatal problem, and all zero values +// if no suitable inteface is found. +func tailscaleInterface() (net.IP, *net.Interface, error) { + ifs, err := net.Interfaces() + if err != nil { + return nil, nil, err + } + for _, iface := range ifs { + if !maybeTailscaleInterfaceName(iface.Name) { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, a := range addrs { + if ipnet, ok := a.(*net.IPNet); ok && isTailscaleIP(ipnet.IP) { + return ipnet.IP, &iface, nil + } + } + } + return nil, nil, nil +} + +// maybeTailscaleInterfaceName reports whether s is an interface +// name that might be used by Tailscale. +func maybeTailscaleInterfaceName(s string) bool { + return strings.HasPrefix(s, "wg") || + strings.HasPrefix(s, "ts") || + strings.HasPrefix(s, "tailscale") +} + +func isTailscaleIP(ip net.IP) bool { + return cgNAT.Contains(ip) +} + +var cgNAT = func() *net.IPNet { + _, ipNet, err := net.ParseCIDR("100.64.0.0/10") + if err != nil { + panic(err) + } + return ipNet +}() + +func handleSSH(s ssh.Session) { + user := s.User() + addr := s.RemoteAddr() + ta, ok := addr.(*net.TCPAddr) + if !ok { + log.Printf("tsshd: rejecting non-TCP addr %T %v", addr, addr) + s.Exit(1) + return + } + if !isTailscaleIP(ta.IP) { + log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP) + s.Exit(1) + return + } + + log.Printf("new session for %q from %v", user, ta) + defer log.Printf("closing session for %q from %v", user, ta) + ptyReq, winCh, isPty := s.Pty() + if !isPty { + fmt.Fprintf(s, "TODO scp etc") + s.Exit(1) + return + } + + userWantsShell := len(s.Command()) == 0 + + if userWantsShell { + shell, err := shellOfUser(s.User()) + if err != nil { + fmt.Fprintf(s, "failed to find shell: %v\n", err) + s.Exit(1) + return + } + cmd := exec.Command(shell) + cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) + f, err := pty.Start(cmd) + if err != nil { + log.Printf("running shell: %v", err) + s.Exit(1) + return + } + defer f.Close() + go func() { + for win := range winCh { + setWinsize(f, win.Width, win.Height) + } + }() + go func() { + io.Copy(f, s) // stdin + }() + io.Copy(s, f) // stdout + cmd.Process.Kill() + if err := cmd.Wait(); err != nil { + s.Exit(1) + } + s.Exit(0) + return + } + + fmt.Fprintf(s, "TODO: args\n") + s.Exit(1) +} + +func shellOfUser(user string) (string, error) { + // TODO + return "/bin/bash", nil +} + +func setWinsize(f *os.File, w, h int) { + syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) +} diff --git a/go.mod b/go.mod index c99636c5c..98762b5c8 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,23 @@ module tailscale.com go 1.13 require ( + github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/gliderlabs/ssh v0.2.2 github.com/go-ole/go-ole v1.2.4 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e github.com/google/go-cmp v0.4.0 github.com/klauspost/compress v1.9.8 + github.com/kr/pty v1.1.1 github.com/mdlayher/netlink v1.1.0 github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3 + github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f github.com/tailscale/wireguard-go v0.0.0-20200211020303-f39bc8eeee1b golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 + golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 gortc.io/stun v1.22.1 honnef.co/go/tools v0.0.1-2019.2.3 // indirect ) diff --git a/go.sum b/go.sum index 4a33de9bd..e2dac0d49 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,16 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4= github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY= github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k= github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8= +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/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= @@ -23,6 +29,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA= github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= @@ -34,6 +41,8 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3 h1:rdtXEo9yffOjh4vZQJw3heaY+ggXKp+zvMX5fihh6lI= github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE= +github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f h1:q2ynfOHxHaaMnkZ1YHswWeO6wEk7IyOnkFozytZ1ztc= +github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE= github.com/tailscale/wireguard-go v0.0.0-20191108062213-b93cdd0582db h1:oP0crfwOb3WZSVrMVm/o51NXN2JirDlcdlNEIPTmgI0= github.com/tailscale/wireguard-go v0.0.0-20200207221558-a158079b156a h1:5TWA3nl2QUfL9OiE3tlBpqJd4GYd4hbGtDNkWQQ2fyc= github.com/tailscale/wireguard-go v0.0.0-20200207221558-a158079b156a/go.mod h1:QPS8HjBzzAXoQNndUNx2efJaQbCCz8nI2Cv1ksTUHyY= @@ -66,8 +75,10 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BG golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -77,6 +88,8 @@ golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..3c8af9794 --- /dev/null +++ b/test.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +function remove_test_files { + rm -f ./*test{,.exe} +} + +function fail { + printf '%s\n' "$1" >&2 + # If we fail, clean up after ourselves + remove_test_files + exit 1 +} + +function main { + test_dirs=() + while IFS= read -r -d '' file + do + dir=$(dirname "$file") + if [[ ! " ${test_dirs[*]} " =~ ${dir} ]]; then + test_dirs+=("$dir") + fi + done < <(find . -type f -iname '*_test.go' -print0) + + for goos in openbsd darwin windows + do + for dir in "${test_dirs[@]}"; do + echo "Testing GOOS=$goos in dir $dir" + GOOS="$goos" go test -c "./$dir" || fail "Test failed using $goos and $dir" + done + done + + # If all goes well, we should still clean up the test files + echo "Test complete" + remove_test_files +} + +main "$@" + diff --git a/wgengine/faketun.go b/wgengine/faketun.go index 01a059f59..8bc04f81f 100644 --- a/wgengine/faketun.go +++ b/wgengine/faketun.go @@ -5,9 +5,10 @@ package wgengine import ( - "github.com/tailscale/wireguard-go/tun" "io" "os" + + "github.com/tailscale/wireguard-go/tun" ) type fakeTun struct { @@ -16,6 +17,9 @@ type fakeTun struct { closechan chan struct{} } +// NewFakeTun returns a fake TUN device that does not depend on the +// operating system or any special permissions. +// It primarily exists for testing. func NewFakeTun() tun.Device { return &fakeTun{ datachan: make(chan []byte), diff --git a/wgengine/ifconfig_windows.go b/wgengine/ifconfig_windows.go index 707a21e38..6ac368f88 100644 --- a/wgengine/ifconfig_windows.go +++ b/wgengine/ifconfig_windows.go @@ -16,13 +16,13 @@ import ( "time" "unsafe" - "github.com/go-ole/go-ole" + ole "github.com/go-ole/go-ole" + winipcfg "github.com/tailscale/winipcfg-go" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "github.com/tailscale/wireguard-go/wgcfg" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" - "golang.zx2c4.com/winipcfg" "tailscale.com/wgengine/winnet" ) diff --git a/wgengine/router_darwin.go b/wgengine/router_darwin.go index c4c09ace4..0c17f28b1 100644 --- a/wgengine/router_darwin.go +++ b/wgengine/router_darwin.go @@ -32,5 +32,6 @@ func (r *darwinRouter) SetRoutes(rs RouteSettings) error { return nil } -func (r *darwinRouter) Close() { +func (r *darwinRouter) Close() error { + return nil } diff --git a/wgengine/router_default.go b/wgengine/router_default.go index 74f993b39..06618460d 100644 --- a/wgengine/router_default.go +++ b/wgengine/router_default.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 !windows,!linux,!darwin +// +build !windows,!linux,!darwin,!openbsd package wgengine @@ -13,5 +13,5 @@ import ( ) func NewUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tuntap tun.Device, netChanged func()) Router { - return NewFakeRouter(logf, tunname, dev, tuntap) + return NewFakeRouter(logf, tunname, dev, tuntap, netChanged) } diff --git a/wgengine/router_fake.go b/wgengine/router_fake.go index 8157e929a..e6227e977 100644 --- a/wgengine/router_fake.go +++ b/wgengine/router_fake.go @@ -16,11 +16,10 @@ type fakeRouter struct { } func NewFakeRouter(logf logger.Logf, tunname string, dev *device.Device, tuntap tun.Device, netChanged func()) Router { - r := fakeRouter{ + return &fakeRouter{ logf: logf, tunname: tunname, } - return &r } func (r *fakeRouter) Up() error { @@ -33,6 +32,7 @@ func (r *fakeRouter) SetRoutes(rs RouteSettings) error { return nil } -func (r *fakeRouter) Close() { +func (r *fakeRouter) Close() error { r.logf("Warning: fakeRouter.Close: not implemented.\n") + return nil } diff --git a/wgengine/router_linux.go b/wgengine/router_linux.go index edd93d570..5c76171c8 100644 --- a/wgengine/router_linux.go +++ b/wgengine/router_linux.go @@ -57,6 +57,8 @@ func cmd(args ...string) *exec.Cmd { func (r *linuxRouter) Up() error { out, err := cmd("ip", "link", "set", r.tunname, "up").CombinedOutput() if err != nil { + // TODO: this should return an error; why is it calling log.Fatalf? + // Audit callers to make sure they're handling errors. log.Fatalf("running ip link failed: %v\n%s", err, out) } @@ -154,6 +156,7 @@ func (r *linuxRouter) SetRoutes(rs RouteSettings) error { r.local = rs.LocalAddr r.routes = newRoutes + // TODO: this: if false { if err := r.replaceResolvConf(rs.DNS, rs.DNSDomains); err != nil { errq = fmt.Errorf("replacing resolv.conf failed: %v", err) @@ -162,12 +165,17 @@ func (r *linuxRouter) SetRoutes(rs RouteSettings) error { return errq } -func (r *linuxRouter) Close() { +func (r *linuxRouter) Close() error { + var ret error r.mon.Close() if err := r.restoreResolvConf(); err != nil { r.logf("failed to restore system resolv.conf: %v", err) + if ret == nil { + ret = err + } } // TODO(apenwarr): clean up iptables etc. + return ret } const ( diff --git a/wgengine/router_openbsd.go b/wgengine/router_openbsd.go new file mode 100644 index 000000000..efb19a179 --- /dev/null +++ b/wgengine/router_openbsd.go @@ -0,0 +1,176 @@ +// 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 wgengine + +import ( + "fmt" + "log" + "net" + "os/exec" + + "github.com/tailscale/wireguard-go/device" + "github.com/tailscale/wireguard-go/tun" + "github.com/tailscale/wireguard-go/wgcfg" + "tailscale.com/logger" +) + +// For now this router only supports the userspace WireGuard implementations. +// +// There is an experimental kernel version in the works: +// https://git.zx2c4.com/wireguard-openbsd. +// +// TODO(mbaillie): netlink-style monitoring might be possible through +// `ifstated(8)`/`devd(8)`, or become possible with the OpenBSD kernel +// implementation. This merits further investigation. + +type openbsdRouter struct { + logf logger.Logf + tunname string + local wgcfg.CIDR + routes map[wgcfg.CIDR]struct{} +} + +func NewUserspaceRouter(logf logger.Logf, tunname string, _ *device.Device, tuntap tun.Device, _ func()) Router { + r := openbsdRouter{ + logf: logf, + tunname: tunname, + } + return &r +} + +// TODO(mbaillie): extract as identical to linux version +func cmd(args ...string) *exec.Cmd { + if len(args) == 0 { + log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]\n", args) + } + return exec.Command(args[0], args[1:]...) +} + +func (r *openbsdRouter) Up() error { + ifup := []string{"ifconfig", r.tunname, "up"} + if out, err := cmd(ifup...).CombinedOutput(); err != nil { + r.logf("running ifconfig failed: %v\n%s", err, out) + return err + } + return nil +} + +func (r *openbsdRouter) SetRoutes(rs RouteSettings) error { + var errq error + + if rs.LocalAddr != r.local { + if r.local != (wgcfg.CIDR{}) { + addrdel := []string{"ifconfig", r.tunname, + "inet", r.local.String(), "-alias"} + out, err := cmd(addrdel...).CombinedOutput() + if err != nil { + r.logf("addr del failed: %v: %v\n%s", addrdel, err, out) + if errq == nil { + errq = err + } + } + + routedel := []string{"route", "-q", "-n", + "del", "-inet", r.local.String(), + "-iface", r.local.IP.String()} + if out, err := cmd(routedel...).CombinedOutput(); err != nil { + r.logf("route del failed: %v: %v\n%s", routedel, err, out) + if errq == nil { + errq = err + } + } + } + + addradd := []string{"ifconfig", r.tunname, + "inet", rs.LocalAddr.String(), "alias"} + out, err := cmd(addradd...).CombinedOutput() + if err != nil { + r.logf("addr add failed: %v: %v\n%s", addradd, err, out) + if errq == nil { + errq = err + } + } + + routeadd := []string{"route", "-q", "-n", + "add", "-inet", rs.LocalAddr.String(), + "-iface", rs.LocalAddr.IP.String()} + if out, err := cmd(routeadd...).CombinedOutput(); err != nil { + r.logf("route add failed: %v: %v\n%s", routeadd, err, out) + if errq == nil { + errq = err + } + } + } + + newRoutes := make(map[wgcfg.CIDR]struct{}) + for _, peer := range rs.Cfg.Peers { + for _, route := range peer.AllowedIPs { + newRoutes[route] = struct{}{} + } + } + for route := range r.routes { + if _, keep := newRoutes[route]; !keep { + net := route.IPNet() + nip := net.IP.Mask(net.Mask) + nstr := fmt.Sprintf("%v/%d", nip, route.Mask) + routedel := []string{"route", "-q", "-n", + "del", "-inet", nstr, + "-iface", rs.LocalAddr.IP.String()} + out, err := cmd(routedel...).CombinedOutput() + if err != nil { + r.logf("route del failed: %v: %v\n%s", routedel, err, out) + if errq == nil { + errq = err + } + } + } + } + for route := range newRoutes { + if _, exists := r.routes[route]; !exists { + net := route.IPNet() + nip := net.IP.Mask(net.Mask) + nstr := fmt.Sprintf("%v/%d", nip, route.Mask) + routeadd := []string{"route", "-q", "-n", + "add", "-inet", nstr, + "-iface", rs.LocalAddr.IP.String()} + out, err := cmd(routeadd...).CombinedOutput() + if err != nil { + r.logf("addr add failed: %v: %v\n%s", routeadd, err, out) + if errq == nil { + errq = err + } + } + } + } + + r.local = rs.LocalAddr + r.routes = newRoutes + + if err := r.replaceResolvConf(rs.DNS, rs.DNSDomains); err != nil { + errq = fmt.Errorf("replacing resolv.conf failed: %v", err) + } + + return errq +} + +func (r *openbsdRouter) Close() error { + out, err := cmd("ifconfig", r.tunname, "down").CombinedOutput() + if err != nil { + r.logf("running ifconfig failed: %v\n%s", err, out) + } + + if err := r.restoreResolvConf(); err != nil { + r.logf("failed to restore system resolv.conf: %v", err) + } + + // TODO(mbaillie): wipe routes + + return nil +} + +// TODO(mbaillie): these are no-ops for now. They could re-use the Linux funcs +// (sans systemd parts), but I note Linux DNS is disabled(?) so leaving for now. +func (r *openbsdRouter) replaceResolvConf(_ []net.IP, _ []string) error { return nil } +func (r *openbsdRouter) restoreResolvConf() error { return nil } diff --git a/wgengine/router_windows.go b/wgengine/router_windows.go index c05b81b3e..7e04d823e 100644 --- a/wgengine/router_windows.go +++ b/wgengine/router_windows.go @@ -7,9 +7,9 @@ package wgengine import ( "log" + winipcfg "github.com/tailscale/winipcfg-go" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" - "golang.zx2c4.com/winipcfg" "tailscale.com/logger" ) @@ -43,7 +43,7 @@ func (r *winRouter) Up() error { } func (r *winRouter) SetRoutes(rs RouteSettings) error { - err := ConfigureInterface(&rs.Cfg, r.nativeTun, rs.DNS, rs.DNSDomains) + err := ConfigureInterface(rs.Cfg, r.nativeTun, rs.DNS, rs.DNSDomains) if err != nil { r.logf("ConfigureInterface: %v\n", err) return err @@ -51,8 +51,9 @@ func (r *winRouter) SetRoutes(rs RouteSettings) error { return nil } -func (r *winRouter) Close() { +func (r *winRouter) Close() error { if r.routeChangeCallback != nil { r.routeChangeCallback.Unregister() } + return nil } diff --git a/wgengine/rtnlmon/mon.go b/wgengine/rtnlmon/mon.go index 080ed2362..b06f1f47f 100644 --- a/wgengine/rtnlmon/mon.go +++ b/wgengine/rtnlmon/mon.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build !windows + // Package rtnlmon watches for "interesting" changes to the network // stack and fires a callback. package rtnlmon diff --git a/wgengine/rusage.go b/wgengine/rusage.go index 98df2afa4..698f9ebf4 100644 --- a/wgengine/rusage.go +++ b/wgengine/rusage.go @@ -11,7 +11,10 @@ import ( "tailscale.com/logger" ) -func RusagePrefixLog(logf logger.Logf) func(f string, argv ...interface{}) { +// RusagePrefixLog returns a Logf func wrapping the provided logf func that adds +// a prefixed log message to each line with the current binary memory usage +// and max RSS. +func RusagePrefixLog(logf logger.Logf) logger.Logf { return func(f string, argv ...interface{}) { var m runtime.MemStats runtime.ReadMemStats(&m) diff --git a/wgengine/rusage_nowindows.go b/wgengine/rusage_nowindows.go index f6dd89b4b..54f17e889 100644 --- a/wgengine/rusage_nowindows.go +++ b/wgengine/rusage_nowindows.go @@ -23,7 +23,7 @@ func rusageMaxRSS() float64 { rss /= 1 << 20 // ru_maxrss is bytes on darwin } else { // ru_maxrss is kilobytes elsewhere (linux, openbsd, etc) - rss /= 1024 + rss /= 1 << 10 } return rss } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 45437b44f..d2e6242f9 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -67,14 +67,14 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16, der tuntap, err := tun.CreateTUN(tunname, device.DefaultMTU) if err != nil { - log.Printf("CreateTUN: %v\n", err) + logf("CreateTUN: %v\n", err) return nil, err } - log.Printf("CreateTUN ok.\n") + logf("CreateTUN ok.\n") e, err := NewUserspaceEngineAdvanced(logf, tuntap, NewUserspaceRouter, listenPort, derp) if err != nil { - log.Printf("NewUserspaceEngineAdv: %v\n", err) + logf("NewUserspaceEngineAdv: %v\n", err) return nil, err } return e, err @@ -205,7 +205,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, dnsDomains []string) error e.peerSequence[i] = p.PublicKey } - // TODO(apenwarr): get rid of silly uapi stuff for in-process comms + // TODO(apenwarr): get rid of uapi stuff for in-process comms uapi, err := cfg.ToUAPI() if err != nil { return err @@ -239,7 +239,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, dnsDomains []string) error rs := RouteSettings{ LocalAddr: cidr, - Cfg: *cfg, + Cfg: cfg, DNS: cfg.Interface.Dns, DNSDomains: dnsDomains, } diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index ee4b23d98..5172f4b29 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -5,9 +5,9 @@ package wgengine import ( - "bytes" "log" "runtime/pprof" + "strings" "time" "github.com/tailscale/wireguard-go/wgcfg" @@ -45,7 +45,7 @@ func (e *watchdogEngine) watchdogErr(name string, fn func() error) error { t.Stop() return err case <-t.C: - buf := new(bytes.Buffer) + buf := new(strings.Builder) pprof.Lookup("goroutine").WriteTo(buf, 1) e.logf("wgengine watchdog stacks:\n%s", buf.String()) e.fatalf("wgengine: watchdog timeout on %s", name) diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index e431bdf41..1971bd8d4 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -14,6 +14,10 @@ import ( "tailscale.com/wgengine/filter" ) +// ByteCount is the number of bytes that have been sent or received. +// +// TODO: why is this a type? remove? +// TODO: document whether it's payload bytes only or if it includes framing overhead. type ByteCount int64 type PeerStatus struct { @@ -22,20 +26,29 @@ type PeerStatus struct { NodeKey tailcfg.NodeKey } +// Status is the Engine status. type Status struct { Peers []PeerStatus LocalAddrs []string // TODO(crawshaw): []wgcfg.Endpoint? } -type StatusCallback func(s *Status, err error) +// StatusCallback is the type of status callbacks used by +// Engine.SetStatusCallback. +// +// Exactly one of Status or error is non-nil. +type StatusCallback func(*Status, error) +// RouteSettings is the full WireGuard config data (set of peers keys, +// IP, etc in wgcfg.Config) plus the things that WireGuard doesn't do +// itself, like DNS stuff. type RouteSettings struct { - LocalAddr wgcfg.CIDR + LocalAddr wgcfg.CIDR // TODO: why is this here? how does it differ from wgcfg.Config's info? DNS []net.IP DNSDomains []string - Cfg wgcfg.Config + Cfg *wgcfg.Config } +// OnlyRelevantParts returns a string minimally describing the route settings. func (rs *RouteSettings) OnlyRelevantParts() string { var peers [][]wgcfg.CIDR for _, p := range rs.Cfg.Peers { @@ -45,31 +58,58 @@ func (rs *RouteSettings) OnlyRelevantParts() string { rs.LocalAddr, rs.DNS, rs.DNSDomains, peers) } +// Router is responsible for managing the system route table. +// +// There's only one instance, and one per-OS implementation. type Router interface { + // Up brings the router up. Up() error - SetRoutes(rs RouteSettings) error - Close() + + // SetRoutes is called regularly on network map updates. + // It's how you kernel route table entries are populated for + // each peer. + SetRoutes(RouteSettings) error + + // Close closes the router. + Close() error } +// Engine is the Tailscale WireGuard engine interface. type Engine interface { - // Reconfigure wireguard and make sure it's running. + // Reconfig reconfigures WireGuard and makes sure it's running. // This also handles setting up any kernel routes. + // + // The provided DNS domains are not part of wgcfg.Config, as + // WireGuard itself doesn't care about such things. + // + // This is called whenever the tailcontrol (control plane) + // sends an updated network map. Reconfig(cfg *wgcfg.Config, dnsDomains []string) error - // Update the packet filter. - SetFilter(filt *filter.Filter) - // Set the function to call when wireguard status changes. - SetStatusCallback(cb StatusCallback) - // Request a wireguard status update right away, sent to the callback. + + // SetFilter updates the packet filter. + SetFilter(*filter.Filter) + + // SetStatusCallback sets the function to call when the + // WireGuard status changes. + SetStatusCallback(StatusCallback) + + // RequestStatus requests a WireGuard status update right + // away, sent to the callback registered via SetStatusCallback. RequestStatus() - // Shut down this wireguard instance, remove any routes it added, etc. - // To bring it up again later, you'll need a new Engine. + + // Close shuts down this wireguard instance, remove any routes + // it added, etc. To bring it up again later, you'll need a + // new Engine. Close() - // Wait until the Engine is .Close()ed or aborts with an error. - // You don't have to call this. + + // Wait waits until the Engine's Close method is called or the + // engine aborts with an error. You don't have to call this. + // TODO: return an error? Wait() + // LinkChange informs the engine that the system network // link has changed. The isExpensive parameter is set on links - // where sending packets uses substantial power or dollars - // (such as LTE on a phone). + // where sending packets uses substantial power or money, + // such as mobile data on a phone. LinkChange(isExpensive bool) }