Move Linux client & common packages into a public repo.

pull/11/head
Earl Lee 4 years ago
parent c955043dfe
commit a8d8b8719a

@ -0,0 +1,17 @@
# This is the official list of Tailscale
# authors for copyright purposes.
#
# Names should be added to this file as one of
# Organization's name
# Individual's name <submission email address>
# Individual's name <submission email address> <email2> <emailN>
#
# Please keep the list sorted.
#
# You do not need to add entries to this list, and we don't actively
# populate this list. If you do want to be acknowledged explicitly as
# a copyright holder, though, then please send a PR referencing your
# earlier contributions and clarifying whether it's you or your
# company that owns the rights to your contribution.
Tailscale Inc.

@ -0,0 +1,27 @@
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Tailscale Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,24 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Tailscale Inc. as part of the Tailscale project.
Tailscale Inc. hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable (except as stated
in this section) patent license to make, have made, use, offer to
sell, sell, import, transfer and otherwise run, modify and propagate
the contents of this implementation of Tailscale, where such license
applies only to those patent claims, both currently owned or
controlled by Tailscale Inc. and acquired in the future, licensable
by Tailscale Inc. that are necessarily infringed by this
implementation of Tailscale. This grant does not include claims that
would be infringed only as a consequence of further modification of
this implementation. If you or your agent or exclusive licensee
institute or order or agree to the institution of patent litigation
against any entity (including a cross-claim or counterclaim in a
lawsuit) alleging that this implementation of Tailscale or any code
incorporated within this implementation of Tailscale constitutes
direct or contributory patent infringement, or inducement of patent
infringement, then any patent rights granted to you under this License
for this implementation of Tailscale shall terminate as of the date
such litigation is filed.

@ -0,0 +1,28 @@
// Copyright 2019 Tailscale & 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 atomicfile contains code related to writing to filesystems
// atomically.
//
// This package should be considered internal; its API is not stable.
package atomicfile // import "tailscale.com/atomicfile"
import (
"fmt"
"io/ioutil"
"os"
)
// WriteFile writes data to filename+some suffix, then renames it
// into filename.
func WriteFile(filename string, data []byte, perm os.FileMode) error {
tmpname := filename + ".new.tmp"
if err := ioutil.WriteFile(tmpname, data, perm); err != nil {
return fmt.Errorf("%#v: %v", tmpname, err)
}
if err := os.Rename(tmpname, filename); err != nil {
return fmt.Errorf("%#v->%#v: %v", tmpname, filename, err)
}
return nil
}

@ -0,0 +1,14 @@
/*.tar.gz
/*.deb
/*.rpm
/*.spec
/pkgver
debian/changelog
debian/debhelper-build-stamp
debian/files
debian/*.log
debian/*.substvars
debian/*.debhelper
debian/tailscale-relay
/tailscale-relay/
/tailscale-relay-*

@ -0,0 +1,63 @@
{
// Declare static groups of users beyond those in the identity service
"Groups": {
"group:eng": ["u1@example.com", "u2@example.com"]
},
// Declare convenient hostname aliases to use in place of IP addresses
"Hosts": {
"h222": "100.2.2.2"
},
// Access control list
"ACLs": [
{
"Action": "accept",
// Match any of several users
"Users": ["a@example.com", "b@example.com"],
// Match any port on h222, and port 22 of 10.1.2.3
"Ports": ["h222:*", "10.1.2.3:22"]
},
{
"Action": "accept",
// Match any user at all
"Users": ["*"],
// Match port 80 on one machine, ports 53 and 5353 on a second one,
// and ports 8000 through 8080 (a port range) on a third one.
"Ports": ["h222:80", "10.8.8.8:53,5353", "10.2.3.4:8000-8080"]
},
{
"Action": "accept",
// Match all users in the "Admin" role (network administrators)
"Users": ["role:Admin", "group:eng"],
// Allow access to port 22 on all servers
"Ports": ["*:22"]
},
{
"Action": "accept",
"Users": ["role:User"],
// Match only windows and linux workstations (not implemented yet)
"OS": ["windows", "linux"],
// Only desktop machines are allowed to access this server
"Ports": ["10.1.1.1:443"]
},
{
"Action": "accept",
"Users": ["*"],
// Match machines which have never been authorized, or which expired.
// (not implemented yet)
"MachineAuth": ["unauthorized", "expired"],
// Logged-in users on unauthorized machines can access the email server.
// Open the TLS ports for SMTP, IMAP, and HTTP.
"Ports": ["10.1.2.3:465", "10.1.2.3:993", "10.1.2.3:443"]
},
// Match absolutely everything. Comment out this section if you want
// the above ACLs to apply.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
// Leave this line here so that every rule can end in a comma.
// It has no effect since it has no matching rules.
{"Action": "accept"}
]
}

@ -0,0 +1 @@
rm -f debian/changelog *~ debian/*~

@ -0,0 +1,13 @@
exec >&2
read -r package <package
rm -f *~ .*~ \
debian/*~ debian/changelog debian/debhelper-build-stamp \
debian/*.log debian/files debian/*.substvars debian/*.debhelper \
*.tar.gz *.deb *.rpm *.spec pkgver relaynode *.exe
[ -n "$package" ] && rm -rf "debian/$package"
for d in */.stamp; do
if [ -e "$d" ]; then
dir=$(dirname "$d")
rm -rf "$dir"
fi
done

@ -0,0 +1,10 @@
exec >&2
dir=${1%/*}
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
read -r package <"$S/$dir/package"
read -r version <"$S/oss/version/short.txt"
arch=$(dpkg --print-architecture)
redo-ifchange "$dir/${package}_$arch.deb"
rm -f "$dir/${package}"_*_"$arch.deb"
ln -sf "${package}_$arch.deb" "$dir/${package}_${version}_$arch.deb"

@ -0,0 +1 @@
Tailscale IPN relay daemon.

@ -0,0 +1,5 @@
redo-ifchange ../../../version/short.txt gen-changelog
(
cd ..
debian/gen-changelog
) >$3

@ -0,0 +1,14 @@
Source: tailscale-relay
Section: net
Priority: extra
Maintainer: Avery Pennarun <apenwarr@tailscale.com>
Build-Depends: debhelper (>= 10.2.5), dh-systemd (>= 1.5)
Standards-Version: 3.9.2
Homepage: https://tailscale.com/
Vcs-Git: https://github.com/tailscale/tailscale
Vcs-Browser: https://github.com/tailscale/tailscale
Package: tailscale-relay
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Traffic relay node for Tailscale IPN

@ -0,0 +1,11 @@
Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173
Upstream-Name: tailscale-relay
Upstream-Contact: Avery Pennarun <apenwarr@tailscale.com>
Source: https://github.com/tailscale/tailscale/
Files: *
Copyright: © 2019 Tailscale Inc. <info@tailscale.com>
License: Proprietary
*
* Copyright 2019 Tailscale Inc. All rights reserved.
*

@ -0,0 +1,25 @@
#!/bin/sh
read junk pkgname <debian/control
read shortver <../../version/short.txt
git log --pretty='format:'"$pkgname"' (SHA:%H) unstable; urgency=low
* %s
-- %aN <%aE> %aD
' . |
python -Sc '
import os, re, subprocess, sys
first = True
def Describe(g):
global first
if first:
s = sys.argv[1]
first = False
else:
sha = g.group(1)
s = subprocess.check_output(["git", "describe", "--", sha]).strip().decode("utf-8")
return re.sub(r"^\D*", "", s)
print(re.sub(r"SHA:([0-9a-f]+)", Describe, sys.stdin.read()))
' "$shortver"

@ -0,0 +1,4 @@
relaynode /usr/sbin
tailscale-login /usr/sbin
taillogin /usr/sbin
acl.json /etc/tailscale

@ -0,0 +1,8 @@
#DEBHELPER#
f=/var/lib/tailscale/relay.conf
if ! [ -e "$f" ]; then
echo
echo "Note: Run tailscale-login to configure $f." >&2
echo
fi

@ -0,0 +1,10 @@
#!/usr/bin/make -f
DESTDIR=debian/tailscale-relay
override_dh_auto_test:
override_dh_auto_install:
mkdir -p "${DESTDIR}/etc/default"
cp tailscale-relay.defaults "${DESTDIR}/etc/default/tailscale-relay"
%:
dh $@ --with=systemd

@ -0,0 +1,12 @@
[Unit]
Description=Traffic relay node for Tailscale IPN
After=network.target
ConditionPathExists=/var/lib/tailscale/relay.conf
[Service]
EnvironmentFile=/etc/default/tailscale-relay
ExecStart=/usr/sbin/relaynode --config=/var/lib/tailscale/relay.conf --tun=wg0 $PORT $ACL_FILE $FLAGS
Restart=on-failure
[Install]
WantedBy=multi-user.target

@ -0,0 +1,20 @@
exec >&2
dir=${1%/*}
redo-ifchange "$S/oss/version/short.txt" "$S/$dir/package" "$dir/debtmp.dir"
read -r package <"$S/$dir/package"
read -r version <"$S/oss/version/short.txt"
arch=$(dpkg --print-architecture)
(
cd "$S/$dir"
git ls-files debian | xargs redo-ifchange debian/changelog
)
cp -a "$S/$dir/debian" "$dir/debtmp/"
rm -f "$dir/debtmp/debian/$package.debhelper.log"
(
cd "$dir/debtmp" &&
debian/rules build &&
fakeroot debian/rules binary
)
mv "$dir/${package}_${version}_${arch}.deb" "$3"

@ -0,0 +1,21 @@
# Generate a directory tree suitable for forming a tarball of
# this package.
exec >&2
dir=${1%/*}
outdir=$PWD/${1%.dir}
rm -rf "$outdir"
mkdir "$outdir"
touch $outdir/.stamp
sfiles="
tailscale-login
acl.json
debian/*.service
*.defaults
"
ofiles="
relaynode
../taillogin/taillogin
"
redo-ifchange "$outdir/.stamp"
(cd "$S/$dir" && redo-ifchange $sfiles && cp $sfiles "$outdir/")
(cd "$dir" && redo-ifchange $ofiles && cp $ofiles "$outdir/")

@ -0,0 +1,14 @@
exec >&2
dir=${1%/*}
pkg=${1##*/}
pkg=${pkg%.rpm}
redo-ifchange "$S/oss/version/short.txt" "$dir/$pkg.tar.gz" "$dir/$pkg.spec"
read -r pkgver junk <"$S/oss/version/short.txt"
machine=$(uname -m)
rpmbase=$HOME/rpmbuild
mkdir -p "$rpmbase/SOURCES/"
cp "$dir/$pkg.tar.gz" "$rpmbase/SOURCES/"
rpmbuild -bb "$dir/$pkg.spec"
mv "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm" $3

@ -0,0 +1,7 @@
redo-ifchange "$S/$1.in" "$S/oss/version/short.txt"
read -r pkgver junk <"$S/oss/version/short.txt"
basever=${pkgver%-*}
subver=${pkgver#*-}
sed -e "s/Version: 0.00$/Version: $basever/" \
-e "s/Release: 0$/Release: $subver/" \
<"$S/$1.in" >"$3"

@ -0,0 +1,8 @@
exec >&2
xdir=${1%.tar.gz}
base=${xdir##*/}
updir=${xdir%/*}
redo-ifchange "$xdir.dir"
OUT="$PWD/$3"
cd "$updir" && tar -czvf "$OUT" --exclude "$base/.stamp" "$base"

@ -0,0 +1,15 @@
# Build packages for customer distribution.
dir=${1%/*}
cd "$dir"
targets="tarball"
if which dh_clean fakeroot dpkg >/dev/null; then
targets="$targets deb"
else
echo "Skipping debian packages: debhelper and/or dpkg build tools missing." >&2
fi
if which rpm >/dev/null; then
targets="$targets rpm"
else
echo "Skipping rpm packages: rpm build tools missing." >&2
fi
redo-ifchange $targets

@ -0,0 +1 @@
/relaynode

@ -0,0 +1,17 @@
# 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 with: docker build -t tailcontrol-alpine .
# Run with: docker run --cap-add=NET_ADMIN --device=/dev/net/tun:/dev/net/tun -it tailcontrol-alpine
FROM debian:stretch-slim
RUN apt-get update && apt-get -y install iproute2 iptables
RUN apt-get -y install ca-certificates
RUN apt-get -y install nginx-light
COPY relaynode /
# tailcontrol -tun=wg0 -dbdir=$HOME/taildb >> tailcontrol.log 2>&1 &
CMD ["/relaynode", "-R", "--config", "relay.conf"]

@ -0,0 +1 @@
redo-ifchange build

@ -0,0 +1,3 @@
exec >&2
redo-ifchange Dockerfile relaynode
docker build -t tailscale .

@ -0,0 +1,2 @@
redo-ifchange ../relaynode
cp ../relaynode $3

@ -0,0 +1,10 @@
#!/bin/sh
# 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.
set -e
redo-ifchange build
docker run --cap-add=NET_ADMIN \
--device=/dev/net/tun:/dev/net/tun \
-it tailscale

@ -0,0 +1 @@
tailscale-relay

@ -0,0 +1,300 @@
// 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.
// Relaynode is the old Linux Tailscale daemon.
//
// Deprecated: this program will be soon deleted. The replacement is
// cmd/tailscaled.
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/apenwarr/fixconsole"
"github.com/google/go-cmp/cmp"
"github.com/klauspost/compress/zstd"
"github.com/pborman/getopt/v2"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
"tailscale.com/control/policy"
"tailscale.com/logpolicy"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
)
func main() {
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Printf("fixConsoleOutput: %v\n", err)
}
config := getopt.StringLong("config", 'f', "", "path to config file")
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)")
tunname := getopt.StringLong("tun", 0, "wg0", "tunnel interface name")
alwaysrefresh := getopt.BoolLong("always-refresh", 0, "force key refresh at startup")
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
routes := getopt.StringLong("routes", 0, "", "list of IP ranges this node can relay")
aclfile := getopt.StringLong("acl-file", 0, "", "restrict traffic relaying according to json ACL file")
derp := getopt.BoolLong("derp", 0, "enable bypass via Detour Encrypted Routing Protocol (DERP)", "false")
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
}
uflags := controlclient.UFlagsHelper(!*nuroutes, *rroutes, *droutes)
if *config == "" {
log.Fatal("no --config file specified")
}
if *tunname == "" {
log.Printf("Warning: no --tun device specified; routing disabled.\n")
}
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
logf := wgengine.RusagePrefixLog(log.Printf)
// The wgengine takes a wireguard configuration produced by the
// controlclient, and runs the actual tunnels and packets.
var e wgengine.Engine
if *fake {
e, err = wgengine.NewFakeUserspaceEngine(logf, *listenport, *derp)
} else {
e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport, *derp)
}
if err != nil {
log.Fatalf("Error starting wireguard engine: %v\n", err)
}
e = wgengine.NewWatchdog(e)
var lastacljson string
var p *policy.Policy
if *aclfile == "" {
e.SetFilter(nil)
} else {
lastacljson = readOrFatal(*aclfile)
p = installFilterOrFatal(e, *aclfile, lastacljson, nil)
}
var lastNetMap *controlclient.NetworkMap
var lastUserMap map[string][]filter.IP
statusFunc := func(new controlclient.Status) {
if new.URL != "" {
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
return
}
if new.Err != "" {
log.Print(new.Err)
return
}
if new.Persist != nil {
if err := saveConfig(*config, *new.Persist); err != nil {
log.Println(err)
}
}
if m := new.NetMap; m != nil {
if lastNetMap != nil {
s1 := strings.Split(lastNetMap.Concise(), "\n")
s2 := strings.Split(new.NetMap.Concise(), "\n")
logf("netmap diff:\n%v\n", cmp.Diff(s1, s2))
}
lastNetMap = m
if m.Equal(&controlclient.NetworkMap{}) {
return
}
wgcfg, err := m.WGCfg(uflags, m.DNS)
if err != nil {
log.Fatalf("Error getting wg config: %v\n", err)
}
err = e.Reconfig(wgcfg, m.DNSDomains)
if err != nil {
log.Fatalf("Error reconfiguring engine: %v\n", err)
}
lastUserMap = m.UserMap()
if p != nil {
matches, err := p.Expand(lastUserMap)
if err != nil {
log.Fatalf("Error expanding ACLs: %v\n", err)
}
e.SetFilter(filter.New(matches))
}
}
}
cfg, err := loadConfig(*config)
if err != nil {
log.Fatal(err)
}
hi := controlclient.NewHostinfo()
hi.FrontendLogID = pol.PublicID.String()
hi.BackendLogID = pol.PublicID.String()
if *routes != "" {
for _, routeStr := range strings.Split(*routes, ",") {
cidr, err := wgcfg.ParseCIDR(routeStr)
if err != nil {
log.Fatalf("--routes: not an IP range: %s", routeStr)
}
hi.RoutableIPs = append(hi.RoutableIPs, *cidr)
}
}
c, err := controlclient.New(controlclient.Options{
Persist: cfg,
ServerURL: *server,
Hostinfo: &hi,
NewDecompressor: func() (controlclient.Decompressor, error) {
return zstd.NewReader(nil)
},
KeepAlive: true,
})
c.SetStatusFunc(statusFunc)
if err != nil {
log.Fatal(err)
}
lf := controlclient.LoginDefault
if *alwaysrefresh {
lf |= controlclient.LoginInteractive
}
c.Login(nil, lf)
// Print the wireguard status when we get an update.
e.SetStatusCallback(func(s *wgengine.Status, err error) {
if err != nil {
log.Fatalf("Wireguard engine status error: %v\n", err)
}
var ss []string
for _, p := range s.Peers {
if p.LastHandshake.IsZero() {
ss = append(ss, "x")
} else {
ss = append(ss, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
}
}
logf("v%v peers: %v\n", version.LONG, strings.Join(ss, " "))
c.UpdateEndpoints(0, s.LocalAddrs)
})
if *debug != "" {
go runDebugServer(*debug)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
signal.Notify(sigCh, syscall.SIGTERM)
t := time.NewTicker(5 * time.Second)
loop:
for {
select {
case <-t.C:
// For the sake of curiosity, request a status
// update periodically.
e.RequestStatus()
// check if aclfile has changed.
// TODO(apenwarr): use fsnotify instead of polling?
if *aclfile != "" {
json := readOrFatal(*aclfile)
if json != lastacljson {
logf("ACL file (%v) changed. Reloading filter.\n", *aclfile)
lastacljson = json
p = installFilterOrFatal(e, *aclfile, json, lastUserMap)
}
}
case <-sigCh:
logf("signal received, exiting")
t.Stop()
break loop
}
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
e.Close()
pol.Shutdown(ctx)
}
func loadConfig(path string) (cfg controlclient.Persist, err error) {
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
log.Printf("config %s does not exist", path)
return controlclient.Persist{}, nil
}
if err := json.Unmarshal(b, &cfg); err != nil {
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
}
return cfg, nil
}
func saveConfig(path string, cfg controlclient.Persist) error {
b, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
return fmt.Errorf("save config: %v", err)
}
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
return fmt.Errorf("save config: %v", err)
}
return nil
}
func readOrFatal(filename string) string {
b, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("%v: ReadFile: %v\n", filename, err)
}
return string(b)
}
func installFilterOrFatal(e wgengine.Engine, filename, acljson string, usermap map[string][]filter.IP) *policy.Policy {
p, err := policy.Parse(acljson)
if err != nil {
log.Fatalf("%v: json filter: %v\n", filename, err)
}
matches, err := p.Expand(usermap)
if err != nil {
log.Fatalf("%v: json filter: %v\n", filename, err)
}
e.SetFilter(filter.New(matches))
return p
}
func runDebugServer(addr string) {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
srv := http.Server{
Addr: addr,
Handler: mux,
}
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

@ -0,0 +1,9 @@
exec >&2
dir=${2%/*}
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
read -r package <"$S/$dir/package"
read -r pkgver <"$S/oss/version/short.txt"
machine=$(uname -m)
redo-ifchange "$dir/$package.rpm"
rm -f "$dir/${package}"-*."$machine.rpm"
ln -sf "$package.rpm" "$dir/$package-$pkgver.$machine.rpm"

@ -0,0 +1,4 @@
#!/bin/sh
cfg=/var/lib/tailscale/relay.conf
dir=$(dirname "$0")
"$dir/taillogin" --config="$cfg"

@ -0,0 +1,14 @@
# Set the port to listen on for incoming VPN packets.
# Remote nodes will automatically be informed about the new port number,
# but you might want to configure this in order to set external firewall
# settings.
PORT="--port=41641"
# Comment out this line to allow all traffic to be relayed.
# Or edit the given file to allow specific traffic.
# The example file is unlikely to match any users on your network, so it
# will block all incoming traffic by default.
ACL_FILE="--acl-file=/etc/tailscale/acl.json"
# Extra flags you might want to pass to relaynode.
FLAGS=""

@ -0,0 +1,42 @@
Name: tailscale-relay
Version: 0.00
Release: 0
Summary: Traffic relay node for Tailscale
Group: Network
License: Proprietary
URL: https://tailscale.com/
Vendor: Tailscale Inc.
#Source: https://github.com/tailscale/tailscale
Source0: tailscale-relay.tar.gz
#Prefix: %{_prefix}
Packager: Avery Pennarun <apenwarr@tailscale.com>
BuildRoot: %{_tmppath}/%{name}-root
%description
Traffic relay node for Tailscale.
%prep
%setup -n tailscale-relay
%build
%install
D=$RPM_BUILD_ROOT
[ "$D" = "/" -o -z "$D" ] && exit 99
rm -rf "$D"
mkdir -p $D/usr/sbin $D/lib/systemd/system $D/etc/default $D/etc/tailscale
cp taillogin tailscale-login relaynode $D/usr/sbin
cp tailscale-relay.service $D/lib/systemd/system/
cp tailscale-relay.defaults $D/etc/default/tailscale-relay
cp acl.json $D/etc/tailscale/acl.json
%clean
%files
%defattr(-,root,root)
%config(noreplace) /etc/default/tailscale-relay
%config(noreplace) /etc/tailscale/acl.json
/lib/systemd/system/tailscale-relay.service
/usr/sbin/taillogin
/usr/sbin/tailscale-login
/usr/sbin/relaynode

@ -0,0 +1,7 @@
dir=${1%/*}
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
read -r package <"$S/$dir/package"
read -r version <"$S/oss/version/short.txt"
redo-ifchange "$dir/$package.tar.gz"
rm -f "$dir/$package"-*.tar.gz
ln -sf "$package.tar.gz" "$dir/$package-$version.tar.gz"

@ -0,0 +1,96 @@
// 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.
// The taillogin command, invoked via the tailscale-login shell script, is shipped
// with the current (old) Linux client, to log in to Tailscale on a Linux box.
//
// Deprecated: this will be deleted, to be replaced by cmd/tailscale.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"github.com/pborman/getopt/v2"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
"tailscale.com/logpolicy"
)
func main() {
config := getopt.StringLong("config", 'f', "", "path to config file")
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailgate server")
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatal("too many non-flag arguments")
}
if *config == "" {
log.Fatal("no --config file specified")
}
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
defer pol.Close()
cfg, err := loadConfig(*config)
if err != nil {
log.Fatal(err)
}
hi := controlclient.NewHostinfo()
hi.FrontendLogID = pol.PublicID.String()
hi.BackendLogID = pol.PublicID.String()
done := make(chan struct{}, 1)
c, err := controlclient.New(controlclient.Options{
Persist: cfg,
ServerURL: *server,
Hostinfo: &hi,
})
c.SetStatusFunc(func(new controlclient.Status) {
if new.URL != "" {
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
return
}
if new.Err != "" {
log.Print(new.Err)
return
}
if new.Persist != nil {
if err := saveConfig(*config, *new.Persist); err != nil {
log.Println(err)
}
}
if new.NetMap != nil {
done <- struct{}{}
}
})
c.Login(nil, 0)
<-done
log.Printf("Success.\n")
}
func loadConfig(path string) (cfg controlclient.Persist, err error) {
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
log.Printf("config %s does not exist", path)
return controlclient.Persist{}, nil
}
if err := json.Unmarshal(b, &cfg); err != nil {
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
}
return cfg, nil
}
func saveConfig(path string, cfg controlclient.Persist) error {
b, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
return fmt.Errorf("save config: %v", err)
}
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
return fmt.Errorf("save config: %v", err)
}
return nil
}

@ -0,0 +1,149 @@
// 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.
// The tailscale command is the Tailscale command-line client. It interacts
// with the tailscaled client daemon.
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"syscall"
"github.com/apenwarr/fixconsole"
"github.com/pborman/getopt/v2"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/logpolicy"
"tailscale.com/safesocket"
)
func pump(ctx context.Context, bc *ipn.BackendClient, c net.Conn) {
defer log.Printf("Control connection done.\n")
defer c.Close()
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(c)
if err != nil {
log.Printf("ReadMsg: %v\n", err)
break
}
bc.GotNotifyMsg(msg)
}
}
func main() {
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Printf("fixConsoleOutput: %v\n", err)
}
config := getopt.StringLong("config", 'f', "", "path to config file")
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
alwaysrefresh := getopt.BoolLong("always-refresh", 0, "force key refresh at startup")
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
getopt.Parse()
if *config == "" {
logpolicy.New("tailnode.log.tailscale.io", "tailscale")
log.Fatal("no --config file specified")
}
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
}
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
defer pol.Close()
prefs, err := loadConfig(*config)
if err != nil {
log.Fatal(err)
}
// TODO(apenwarr): fix different semantics between prefs and uflags
// TODO(apenwarr): allow setting/using CorpDNS
prefs.WantRunning = true
prefs.RouteAll = *rroutes || *droutes
prefs.AllowSingleHosts = !*nuroutes
c, err := safesocket.Connect("", "Tailscale", "tailscaled", 41112)
if err != nil {
log.Fatalf("safesocket.Connect: %v\n", err)
}
clientToServer := func(b []byte) {
ipn.WriteMsg(c, b)
}
ctx, cancel := context.WithCancel(context.Background())
lf := controlclient.LoginDefault
if *alwaysrefresh {
lf |= controlclient.LoginInteractive
}
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
<-interrupt
c.Close()
}()
bc := ipn.NewBackendClient(log.Printf, clientToServer)
opts := ipn.Options{
Prefs: prefs,
ServerURL: *server,
LoginFlags: lf,
Notify: func(n ipn.Notify) {
log.Printf("Notify: %v\n", n)
if n.ErrMessage != nil {
log.Fatalf("backend error: %v\n", *n.ErrMessage)
}
if s := n.State; s != nil {
switch *s {
case ipn.NeedsLogin:
bc.StartLoginInteractive()
case ipn.NeedsMachineAuth:
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", *server)
case ipn.Starting, ipn.Running:
// Done full authentication process
cancel()
}
}
if url := n.BrowseToURL; url != nil {
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
}
if p := n.Prefs; p != nil {
prefs = *p
saveConfig(*config, *p)
}
},
}
bc.Start(opts)
pump(ctx, bc, c)
}
func loadConfig(path string) (ipn.Prefs, error) {
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
log.Printf("config %s does not exist", path)
return ipn.NewPrefs(), nil
}
return ipn.PrefsFromBytes(b, false)
}
func saveConfig(path string, prefs ipn.Prefs) error {
b, err := json.MarshalIndent(prefs, "", "\t")
if err != nil {
return fmt.Errorf("save config: %v", err)
}
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
return fmt.Errorf("save config: %v", err)
}
return nil
}

@ -0,0 +1,88 @@
// 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.
// The tailscaled program is the Tailscale client daemon. It's configured
// and controlled via the tailscale CLI program.
//
// It primarily supports Linux, though other systems will likely be
// supported in the future.
package main
import (
"context"
"log"
"net/http"
"net/http/pprof"
"github.com/apenwarr/fixconsole"
"github.com/pborman/getopt/v2"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/wgengine"
)
func main() {
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
logf := wgengine.RusagePrefixLog(log.Printf)
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
logf("fixConsoleOutput: %v\n", err)
}
pol := logpolicy.New("tailnode.log.tailscale.io", "tailscaled")
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
}
if *debug != "" {
go runDebugServer(*debug)
}
var e wgengine.Engine
if *fake {
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, false)
} else {
e, err = wgengine.NewUserspaceEngine(logf, "ts0", 0, false)
}
if err != nil {
log.Fatalf("wgengine.New: %v\n", err)
}
e = wgengine.NewWatchdog(e)
opts := ipnserver.Options{
SurviveDisconnects: true,
AllowQuit: false,
}
err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e)
if err != nil {
log.Fatalf("tailscaled: %v\n", err)
}
// TODO(crawshaw): It would be nice to start a timeout context the moment a signal
// is received and use that timeout to give us a moment to finish uploading logs
// here. But the signal is handled inside ipnserver.Run, so some plumbing is needed.
ctx, cancel := context.WithCancel(context.Background())
cancel()
pol.Shutdown(ctx)
}
func runDebugServer(addr string) {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
srv := http.Server{
Addr: addr,
Handler: mux,
}
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

@ -0,0 +1,594 @@
// 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 controlclient implements the client for the IPN control plane.
//
// It handles authentication, port picking, and collects the local
// network configuration.
package controlclient
import (
"context"
"encoding/json"
"fmt"
"reflect"
"sync"
"time"
"golang.org/x/oauth2"
"tailscale.com/logger"
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
)
// TODO(apenwarr): eliminate the 'state' variable, as it's now obsolete.
// It's used only by the unit tests.
type state int
const (
stateNew = state(iota)
stateNotAuthenticated
stateAuthenticating
stateURLVisitRequired
stateAuthenticated
stateSynchronized // connected and received map update
)
func (s state) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s state) String() string {
switch s {
case stateNew:
return "state:new"
case stateNotAuthenticated:
return "state:not-authenticated"
case stateAuthenticating:
return "state:authenticating"
case stateURLVisitRequired:
return "state:url-visit-required"
case stateAuthenticated:
return "state:authenticated"
case stateSynchronized:
return "state:synchronized"
default:
return fmt.Sprintf("state:unknown:%d", int(s))
}
}
type Status struct {
LoginFinished *struct{}
Err string
URL string
Persist *Persist // locally persisted configuration
NetMap *NetworkMap // server-pushed configuration
Hostinfo tailcfg.Hostinfo // current Hostinfo data
state state
}
// Equal reports whether s and s2 are equal.
func (s *Status) Equal(s2 *Status) bool {
if s == nil && s2 == nil {
return true
}
return s != nil && s2 != nil &&
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
s.Err == s2.Err &&
s.URL == s2.URL &&
reflect.DeepEqual(s.Persist, s2.Persist) &&
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
s.state == s2.state
}
func (s Status) String() string {
b, err := json.MarshalIndent(s, "", "\t")
if err != nil {
panic(err)
}
return s.state.String() + " " + string(b)
}
type LoginGoal struct {
wantLoggedIn bool // true if we *want* to be logged in
token *oauth2.Token // oauth token to use when logging in
flags LoginFlags // flags to use when logging in
url string // auth url that needs to be visited
}
// Client connects to a tailcontrol server for a node.
type Client struct {
direct *Direct // our interface to the server APIs
timeNow func() time.Time
logf logger.Logf
expiry *time.Time
closed bool
newMapCh chan struct{} // readable when we must restart a map request
mu sync.Mutex // mutex guards the following fields
statusFunc func(Status) // called to update Client status
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
hostinfo tailcfg.Hostinfo
inPollNetMap bool // true if currently running a PollNetMap
inSendStatus int // number of sendStatus calls currently in progress
state state
authCtx context.Context // context used for auth requests
mapCtx context.Context // context used for netmap requests
authCancel func() // cancel the auth context
mapCancel func() // cancel the netmap context
quit chan struct{} // when closed, goroutines should all exit
authDone chan struct{} // when closed, auth goroutine is done
mapDone chan struct{} // when closed, map goroutine is done
}
// New creates and starts a new Client.
func New(opts Options) (*Client, error) {
c, err := NewNoStart(opts)
if c != nil {
c.Start()
}
return c, err
}
// NewNoStart creates a new Client, but without calling Start on it.
func NewNoStart(opts Options) (*Client, error) {
direct, err := NewDirect(opts)
if err != nil {
return nil, err
}
c := &Client{
direct: direct,
timeNow: opts.TimeNow,
logf: opts.Logf,
newMapCh: make(chan struct{}, 1),
quit: make(chan struct{}),
authDone: make(chan struct{}),
mapDone: make(chan struct{}),
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
return c, nil
}
// Start starts the client's goroutines.
//
// It should only be called for clients created by NewNoStart.
func (c *Client) Start() {
go c.authRoutine()
go c.mapRoutine()
}
func (c *Client) cancelAuth() {
c.mu.Lock()
if c.authCancel != nil {
c.authCancel()
}
if !c.closed {
c.authCtx, c.authCancel = context.WithCancel(context.Background())
}
c.mu.Unlock()
}
func (c *Client) cancelMapLocked() {
if c.mapCancel != nil {
c.mapCancel()
}
if !c.closed {
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
}
}
func (c *Client) cancelMapUnsafely() {
c.mu.Lock()
c.cancelMapLocked()
c.mu.Unlock()
}
func (c *Client) cancelMapSafely() {
c.mu.Lock()
defer c.mu.Unlock()
c.logf("cancelMapSafely: synced=%v\n", c.synced)
if c.inPollNetMap == true {
// received at least one netmap since the last
// interruption. That means the server has already
// fully processed our last request, which might
// include UpdateEndpoints(). Interrupt it and try
// again.
c.cancelMapLocked()
} else {
// !synced means we either haven't done a netmap
// request yet, or it hasn't answered yet. So the
// server is in an undefined state. If we send
// another netmap request too soon, it might race
// with the last one, and if we're very unlucky,
// the new request will be applied before the old one,
// and the wrong endpoints will get registered. We
// have to tell the client to abort politely, only
// after it receives a response to its existing netmap
// request.
select {
case c.newMapCh <- struct{}{}:
c.logf("cancelMapSafely: wrote to channel\n")
default:
// if channel write failed, then there was already
// an outstanding newMapCh request. One is enough,
// since it'll always use the latest endpoints.
c.logf("cancelMapSafely: channel was full\n")
}
}
}
func (c *Client) authRoutine() {
defer close(c.authDone)
bo := backoff.Backoff{Name: "authRoutine"}
for {
c.mu.Lock()
c.logf("authRoutine: %s\n", c.state)
expiry := c.expiry
goal := c.loginGoal
ctx := c.authCtx
synced := c.synced
c.mu.Unlock()
select {
case <-c.quit:
c.logf("authRoutine: quit\n")
return
default:
}
report := func(err error, msg string) {
c.logf("%s: %v\n", msg, err)
err = fmt.Errorf("%s: %v", msg, err)
// don't send status updates for context errors,
// since context cancelation is always on purpose.
if ctx.Err() == nil {
c.sendStatus("authRoutine1", err, "", nil)
}
}
if goal == nil {
// Wait for something interesting to happen
var exp <-chan time.Time
if expiry != nil && !expiry.IsZero() {
// if expiry is in the future, don't delay
// past that time.
// If it's in the past, then it's already
// being handled by someone, so no need to
// wake ourselves up again.
now := c.timeNow()
if expiry.Before(now) {
delay := expiry.Sub(now)
if delay > 5*time.Second {
delay = time.Second
}
exp = time.After(delay)
}
}
select {
case <-ctx.Done():
c.logf("authRoutine: context done.\n")
case <-exp:
// Unfortunately the key expiry isn't provided
// by the control server until mapRequest.
// So we have to do some hackery with c.expiry
// in here.
// TODO(apenwarr): add a key expiry field in RegisterResponse.
c.logf("authRoutine: key expiration check.\n")
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
c.logf("Key expired; setting loggedIn=false.")
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: c.loggedIn,
}
c.loggedIn = false
c.expiry = nil
c.mu.Unlock()
}
}
} else if !goal.wantLoggedIn {
err := c.direct.TryLogout(c.authCtx)
if err != nil {
report(err, "TryLogout")
bo.BackOff(ctx, err)
continue
}
// success
c.mu.Lock()
c.loggedIn = false
c.loginGoal = nil
c.state = stateNotAuthenticated
c.synced = false
c.mu.Unlock()
c.sendStatus("authRoutine2", nil, "", nil)
bo.BackOff(ctx, nil)
} else { // ie. goal.wantLoggedIn
c.mu.Lock()
if goal.url != "" {
c.state = stateURLVisitRequired
} else {
c.state = stateAuthenticating
}
c.mu.Unlock()
var url string
var err error
var f string
if goal.url != "" {
url, err = c.direct.WaitLoginURL(ctx, goal.url)
f = "WaitLoginURL"
} else {
url, err = c.direct.TryLogin(ctx, goal.token, goal.flags)
f = "TryLogin"
}
if err != nil {
report(err, f)
bo.BackOff(ctx, err)
continue
} else if url != "" {
if goal.url != "" {
err = fmt.Errorf("weird: server required a new url?")
report(err, "WaitLoginURL")
}
goal.url = url
goal.token = nil
goal.flags = LoginDefault
c.mu.Lock()
c.loginGoal = goal
c.state = stateURLVisitRequired
c.synced = false
c.mu.Unlock()
c.sendStatus("authRoutine3", err, url, nil)
bo.BackOff(ctx, err)
continue
}
// success
c.mu.Lock()
c.loggedIn = true
c.loginGoal = nil
c.state = stateAuthenticated
c.mu.Unlock()
c.sendStatus("authRoutine4", nil, "", nil)
c.cancelMapSafely()
bo.BackOff(ctx, nil)
}
}
}
func (c *Client) mapRoutine() {
defer close(c.mapDone)
bo := backoff.Backoff{Name: "mapRoutine"}
for {
c.mu.Lock()
c.logf("mapRoutine: %s\n", c.state)
loggedIn := c.loggedIn
ctx := c.mapCtx
c.mu.Unlock()
select {
case <-c.quit:
c.logf("mapRoutine: quit\n")
return
default:
}
report := func(err error, msg string) {
c.logf("%s: %v\n", msg, err)
err = fmt.Errorf("%s: %v", msg, err)
// don't send status updates for context errors,
// since context cancelation is always on purpose.
if ctx.Err() == nil {
c.sendStatus("mapRoutine1", err, "", nil)
}
}
if !loggedIn {
// Wait for something interesting to happen
c.mu.Lock()
c.synced = false
// c.state is set by authRoutine()
c.mu.Unlock()
select {
case <-ctx.Done():
c.logf("mapRoutine: context done.\n")
case <-c.newMapCh:
c.logf("mapRoutine: new map needed while idle.\n")
}
} else {
// Be sure this is false when we're not inside
// PollNetMap, so that cancelMapSafely() can notify
// us correctly.
c.mu.Lock()
c.inPollNetMap = false
c.mu.Unlock()
err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) {
c.mu.Lock()
select {
case <-c.newMapCh:
c.logf("mapRoutine: new map request during PollNetMap. canceling.\n")
c.cancelMapLocked()
// Don't emit this netmap; we're
// about to request a fresh one.
c.mu.Unlock()
return
default:
}
c.synced = true
c.inPollNetMap = true
if c.loggedIn {
c.state = stateSynchronized
}
exp := nm.Expiry
c.expiry = &exp
stillAuthed := c.loggedIn
state := c.state
c.mu.Unlock()
c.logf("mapRoutine: netmap received: %s\n", state)
if stillAuthed {
c.sendStatus("mapRoutine2", nil, "", nm)
}
})
c.mu.Lock()
c.synced = false
c.inPollNetMap = false
if c.state == stateSynchronized {
c.state = stateAuthenticated
}
c.mu.Unlock()
if err != nil {
report(err, "PollNetMap")
bo.BackOff(ctx, err)
continue
}
bo.BackOff(ctx, nil)
}
}
}
func (c *Client) AuthCantContinue() bool {
c.mu.Lock()
defer c.mu.Unlock()
return !c.loggedIn && (c.loginGoal == nil || c.loginGoal.url != "")
}
func (c *Client) SetStatusFunc(fn func(Status)) {
c.mu.Lock()
c.statusFunc = fn
c.mu.Unlock()
}
func (c *Client) SetHostinfo(hi tailcfg.Hostinfo) {
c.direct.SetHostinfo(hi)
// Send new Hostinfo to server
c.cancelMapSafely()
}
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
c.mu.Lock()
state := c.state
loggedIn := c.loggedIn
synced := c.synced
statusFunc := c.statusFunc
hi := c.hostinfo
c.inSendStatus++
c.mu.Unlock()
c.logf("sendStatus: %s: %v\n", who, state)
var p *Persist
var fin *struct{}
if state == stateAuthenticated {
fin = &struct{}{}
}
if nm != nil && loggedIn && synced {
pp := c.direct.GetPersist()
p = &pp
} else {
// don't send netmap status, as it's misleading when we're
// not logged in.
nm = nil
}
new := Status{
LoginFinished: fin,
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
state: state,
}
if err != nil {
new.Err = err.Error()
}
if statusFunc != nil {
statusFunc(new)
}
c.mu.Lock()
c.inSendStatus--
c.mu.Unlock()
}
func (c *Client) Login(t *oauth2.Token, flags LoginFlags) {
c.logf("client.Login(%v, %v)\n", t != nil, flags)
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: true,
token: t,
flags: flags,
}
c.mu.Unlock()
c.cancelAuth()
}
func (c *Client) Logout() {
c.logf("client.Logout()\n")
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: false,
}
c.mu.Unlock()
c.cancelAuth()
}
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
changed, err := c.direct.SetEndpoints(localPort, endpoints)
if err != nil {
c.sendStatus("updateEndpoints", err, "", nil)
} else if changed {
c.cancelMapSafely()
}
}
func (c *Client) Shutdown() {
c.logf("client.Shutdown()\n")
c.mu.Lock()
inSendStatus := c.inSendStatus
closed := c.closed
if !closed {
c.closed = true
c.statusFunc = nil
}
c.mu.Unlock()
c.logf("client.Shutdown: inSendStatus=%v\n", inSendStatus)
if !closed {
close(c.quit)
c.cancelAuth()
<-c.authDone
c.cancelMapUnsafely()
<-c.mapDone
c.logf("Client.Shutdown done.\n")
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,68 @@
// 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 controlclient
import (
"reflect"
"testing"
)
func fieldsOf(t reflect.Type) (fields []string) {
for i := 0; i < t.NumField(); i++ {
fields = append(fields, t.Field(i).Name)
}
return
}
func TestStatusEqual(t *testing.T) {
// Verify that the Equal method stays in sync with reality
equalHandles := []string{"LoginFinished", "Err", "URL", "Persist", "NetMap", "Hostinfo", "state"}
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, equalHandles)
}
tests := []struct {
a, b *Status
want bool
}{
{
&Status{},
nil,
false,
},
{
nil,
&Status{},
false,
},
{
&Status{},
&Status{},
true,
},
{
&Status{state: stateNew},
&Status{state: stateNew},
true,
},
{
&Status{state: stateNew},
&Status{state: stateAuthenticated},
false,
},
{
&Status{LoginFinished: nil},
&Status{LoginFinished: new(struct{})},
false,
},
}
for i, tt := range tests {
got := tt.a.Equal(tt.b)
if got != tt.want {
t.Errorf("%d. Equal = %v; want %v", i, got, tt.want)
}
}
}

@ -0,0 +1,656 @@
// 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 controlclient
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/nacl/box"
"golang.org/x/oauth2"
"tailscale.com/logger"
"tailscale.com/tailcfg"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
)
type Persist struct {
PrivateMachineKey wgcfg.PrivateKey
PrivateNodeKey wgcfg.PrivateKey
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
Provider string
LoginName string
}
func (p *Persist) Pretty() string {
var mk, ok, nk wgcfg.Key
if !p.PrivateMachineKey.IsZero() {
mk = *p.PrivateMachineKey.Public()
}
if !p.OldPrivateNodeKey.IsZero() {
ok = *p.OldPrivateNodeKey.Public()
}
if !p.PrivateNodeKey.IsZero() {
nk = *p.PrivateNodeKey.Public()
}
return fmt.Sprintf("Persist{m=%v, o=%v, n=%v u=%#v}",
mk.ShortString(), ok.ShortString(), nk.ShortString(),
p.LoginName)
}
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
serverURL string // URL of the tailcontrol server
timeNow func() time.Time
newDecompressor func() (Decompressor, error)
keepAlive bool
logf logger.Logf
mu sync.Mutex // mutex guards the following fields
serverKey wgcfg.Key
persist Persist
tryingNewKey wgcfg.PrivateKey
expiry *time.Time
hostinfo tailcfg.Hostinfo
endpoints []string
localPort uint16
cmdCh chan interface{}
doneCh chan struct{}
}
type Options struct {
Persist Persist // initial persistent data
HTTPC *http.Client // HTTP client used to talk to tailcontrol
ServerURL string // URL of the tailcontrol server
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
}
type Decompressor interface {
DecodeAll(input, dst []byte) ([]byte, error)
Close()
}
// NewDirect returns a new Direct client.
func NewDirect(opts Options) (*Direct, error) {
if opts.ServerURL == "" {
return nil, errors.New("controlclient.New: no server URL specified")
}
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
if opts.HTTPC == nil {
opts.HTTPC = http.DefaultClient
}
if opts.TimeNow == nil {
opts.TimeNow = time.Now
}
if opts.Logf == nil {
// TODO(apenwarr): remove this default and fail instead.
opts.Logf = log.Printf
}
c := &Direct{
httpc: opts.HTTPC,
serverURL: opts.ServerURL,
timeNow: opts.TimeNow,
logf: opts.Logf,
newDecompressor: opts.NewDecompressor,
keepAlive: opts.KeepAlive,
persist: opts.Persist,
}
if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo())
} else {
c.SetHostinfo(*opts.Hostinfo)
}
return c, nil
}
func NewHostinfo() tailcfg.Hostinfo {
hostname, _ := os.Hostname()
os := runtime.GOOS
switch os {
case "darwin":
switch runtime.GOARCH {
case "arm", "arm64":
os = "iOS"
default:
os = "macOS"
}
}
return tailcfg.Hostinfo{
IPNVersion: version.LONG,
Hostname: hostname,
OS: os,
}
}
func (c *Direct) SetHostinfo(hi tailcfg.Hostinfo) {
c.mu.Lock()
defer c.mu.Unlock()
c.logf("Hostinfo: %v\n", hi)
c.hostinfo = hi
}
func (c *Direct) GetPersist() Persist {
c.mu.Lock()
defer c.mu.Unlock()
return c.persist
}
type LoginFlags int
const (
LoginDefault = LoginFlags(0)
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
)
func (c *Direct) TryLogout(ctx context.Context) error {
c.logf("direct.TryLogout()\n")
c.mu.Lock()
defer c.mu.Unlock()
if c.persist.PrivateNodeKey != (wgcfg.PrivateKey{}) {
// TODO(crawshaw): Tell the server. This node key should be immediately invalidated.
}
c.persist = Persist{
PrivateMachineKey: c.persist.PrivateMachineKey,
}
return nil
}
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
c.logf("direct.TryLogin(%v, %v)\n", t != nil, flags)
return c.doLoginOrRegen(ctx, t, flags, false, "")
}
func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newUrl string, err error) {
c.logf("direct.WaitLoginURL\n")
return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url)
}
func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) {
mustregen, url, err := c.doLogin(ctx, t, flags, regen, url)
if err != nil {
return url, err
}
if mustregen {
_, url, err = c.doLogin(ctx, t, flags, true, url)
}
return url, err
}
func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) {
c.mu.Lock()
persist := c.persist
tryingNewKey := c.tryingNewKey
serverKey := c.serverKey
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
if persist.PrivateMachineKey == (wgcfg.PrivateKey{}) {
c.logf("Generating a new machinekey.\n")
mkey, err := wgcfg.NewPrivateKey()
if err != nil {
log.Fatal(err)
}
persist.PrivateMachineKey = *mkey
}
if expired {
c.logf("Old key expired -> regen=true\n")
regen = true
}
if (flags & LoginInteractive) != 0 {
c.logf("LoginInteractive -> regen=true\n")
regen = true
}
c.logf("doLogin(regen=%v, hasUrl=%v)\n", regen, url != "")
if serverKey == (wgcfg.Key{}) {
var err error
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
if err != nil {
return regen, url, err
}
c.mu.Lock()
c.serverKey = serverKey
c.mu.Unlock()
}
var oldNodeKey wgcfg.Key
if url != "" {
} else if regen || persist.PrivateNodeKey == (wgcfg.PrivateKey{}) {
c.logf("Generating a new nodekey.\n")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
key, err := wgcfg.NewPrivateKey()
if err != nil {
c.logf("login keygen: %v", err)
return regen, url, err
}
tryingNewKey = *key
} else {
// Try refreshing the current key first
tryingNewKey = persist.PrivateNodeKey
}
if persist.OldPrivateNodeKey != (wgcfg.PrivateKey{}) {
oldNodeKey = *persist.OldPrivateNodeKey.Public()
}
if tryingNewKey == (wgcfg.PrivateKey{}) {
log.Fatalf("tryingNewKey is empty, give up\n")
}
if c.hostinfo.BackendLogID == "" {
err = errors.New("hostinfo: BackendLogID missing")
return regen, url, err
}
request := tailcfg.RegisterRequest{
Version: 1,
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
NodeKey: tailcfg.NodeKey(*tryingNewKey.Public()),
Hostinfo: c.hostinfo,
Followup: url,
}
c.logf("RegisterReq: onode=%v node=%v fup=%v\n",
request.OldNodeKey.AbbrevString(),
request.NodeKey.AbbrevString(), url != "")
request.Auth.Oauth2Token = t
request.Auth.Provider = persist.Provider
request.Auth.LoginName = persist.LoginName
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
if err != nil {
return regen, url, err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s", c.serverURL, persist.PrivateMachineKey.Public().HexString())
req, err := http.NewRequest("POST", u, body)
if err != nil {
return regen, url, err
}
req = req.WithContext(ctx)
res, err := c.httpc.Do(req)
if err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
}
c.logf("RegisterReq: returned.\n")
resp := tailcfg.RegisterResponse{}
if err := decode(res, &resp, &serverKey, &persist.PrivateMachineKey); err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
}
if resp.NodeKeyExpired {
if regen {
return true, "", fmt.Errorf("weird: regen=true but server says NodeKeyExpired: %v", request.NodeKey)
}
c.logf("server reports new node key %v has expired",
request.NodeKey.AbbrevString())
return true, "", nil
}
if persist.Provider == "" {
persist.Provider = resp.Login.Provider
}
if persist.LoginName == "" {
persist.LoginName = resp.Login.LoginName
}
// TODO(crawshaw): RegisterResponse should be able to mechanically
// communicate some extra instructions from the server:
// - new node key required
// - machine key no longer supported
// - user is disabled
if resp.AuthURL != "" {
c.logf("AuthURL is %.20v...\n", resp.AuthURL)
} else {
c.logf("No AuthURL\n")
}
c.mu.Lock()
if resp.AuthURL == "" {
// key rotation is complete
persist.PrivateNodeKey = tryingNewKey
} else {
// save it for the retry-with-URL
c.tryingNewKey = tryingNewKey
}
c.persist = persist
c.mu.Unlock()
if err != nil {
return regen, "", err
}
if ctx.Err() != nil {
return regen, "", ctx.Err()
}
return false, resp.AuthURL, nil
}
func sameStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func (c *Direct) newEndpoints(localPort uint16, endpoints []string) bool {
c.mu.Lock()
defer c.mu.Unlock()
// Nothing new?
if c.localPort == localPort && sameStrings(c.endpoints, endpoints) {
return false // unchanged
}
c.logf("client.newEndpoints(%v, %v)\n", localPort, endpoints)
if len(c.endpoints) > 0 {
// empty the old list without deallocating it
c.endpoints = c.endpoints[:0]
}
c.localPort = localPort
c.endpoints = append(c.endpoints, endpoints...)
return true // changed
}
// SetEndpoints updates the list of locally advertised endpoints.
// It won't be replicated to the server until a *fresh* call to PollNetMap().
// You don't need to restart PollNetMap if we return changed==false.
func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed bool, err error) {
// (no log message on function entry, because it clutters the logs
// if endpoints haven't changed. newEndpoints() will log it.)
changed = c.newEndpoints(localPort, endpoints)
return changed, nil
}
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
c.mu.Lock()
persist := c.persist
serverURL := c.serverURL
serverKey := c.serverKey
hostinfo := c.hostinfo
localPort := c.localPort
ep := append([]string(nil), c.endpoints...)
c.mu.Unlock()
if hostinfo.BackendLogID == "" {
return errors.New("hostinfo: BackendLogID missing")
}
allowStream := maxPolls != 1
c.logf("PollNetMap: stream=%v :%v %v\n", maxPolls, localPort, ep)
request := tailcfg.MapRequest{
Version: 4,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(*persist.PrivateNodeKey.Public()),
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
}
if c.newDecompressor != nil {
request.Compress = "zstd"
}
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
if err != nil {
return err
}
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
if err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
req = req.WithContext(ctx)
res, err := c.httpc.Do(req)
if err != nil {
return err
}
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("initial fetch failed %d: %s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
defer res.Body.Close()
// If we go more than pollTimeout without hearing from the server,
// end the long poll. We should be receiving a keep alive ping
// every minute.
const pollTimeout = 120 * time.Second
timeout := time.NewTimer(pollTimeout)
timeoutReset := make(chan struct{})
defer close(timeoutReset)
go func() {
for {
select {
case <-timeout.C:
c.logf("map response long-poll timed out!")
cancel()
return
case _, ok := <-timeoutReset:
if !ok {
return // channel closed, shut down goroutine
}
if !timeout.Stop() {
<-timeout.C
}
timeout.Reset(pollTimeout)
}
}
}()
// If allowStream, then the server will use an HTTP long poll to
// return incremental results. There is always one response right
// away, followed by a delay, and eventually others.
// If !allowStream, it'll still send the first result in exactly
// the same format before just closing the connection.
// We can use this same read loop either way.
var msg []byte
for i := 0; i < maxPolls || maxPolls < 0; i++ {
var siz [4]byte
if _, err := io.ReadFull(res.Body, siz[:]); err != nil {
return err
}
size := binary.LittleEndian.Uint32(siz[:])
msg = append(msg[:0], make([]byte, size)...)
if _, err := io.ReadFull(res.Body, msg); err != nil {
return err
}
var resp tailcfg.MapResponse
// Default filter if the key is missing from the incoming
// json (ie. old tailcontrol server without PacketFilter
// support). If even an empty PacketFilter is provided, this
// will be overwritten.
// TODO(apenwarr 2020-02-01): remove after tailcontrol is fully deployed.
resp.PacketFilter = filter.MatchAllowAll
if err := c.decodeMsg(msg, &resp); err != nil {
return err
}
if resp.KeepAlive {
c.logf("map response keep alive received")
timeoutReset <- struct{}{}
continue
}
nm := &NetworkMap{
NodeKey: tailcfg.NodeKey(*persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
Expiry: resp.Node.KeyExpiry,
Addresses: resp.Node.Addresses,
Peers: resp.Peers,
LocalPort: localPort,
User: resp.Node.User,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
Roles: resp.Roles,
DNS: resp.DNS,
DNSDomains: resp.SearchPaths,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: resp.PacketFilter,
}
for _, profile := range resp.UserProfiles {
nm.UserProfiles[profile.ID] = profile
}
if resp.Node.MachineAuthorized {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
//c.logf("new network map[%d]:\n%s", i, nm.Concise())
c.mu.Lock()
c.expiry = &nm.Expiry
c.mu.Unlock()
cb(nm)
}
if ctx.Err() != nil {
return ctx.Err()
}
return nil
}
func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
defer res.Body.Close()
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return err
}
if res.StatusCode != 200 {
return fmt.Errorf("%d: %v", res.StatusCode, string(msg))
}
return decodeMsg(msg, v, serverKey, mkey)
}
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
mkey := c.persist.PrivateMachineKey
serverKey := c.serverKey
decrypted, err := decryptMsg(msg, &serverKey, &mkey)
if err != nil {
return err
}
var b []byte
if c.newDecompressor == nil {
b = decrypted
} else {
//decoder, err := zstd.NewReader(nil)
decoder, err := c.newDecompressor()
if err != nil {
return err
}
defer decoder.Close()
b, err = decoder.DecodeAll(decrypted, nil)
if err != nil {
return err
}
}
if err := json.Unmarshal(b, v); err != nil {
return fmt.Errorf("response: %v", err)
}
return nil
}
func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
decrypted, err := decryptMsg(msg, serverKey, mkey)
if err != nil {
return err
}
if err := json.Unmarshal(decrypted, v); err != nil {
return fmt.Errorf("response: %v", err)
}
return nil
}
func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
var nonce [24]byte
if len(msg) < len(nonce)+1 {
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
}
copy(nonce[:], msg)
msg = msg[len(nonce):]
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
if !ok {
return nil, fmt.Errorf("cannot decrypt response")
}
return decrypted, nil
}
func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
msg := box.Seal(nonce[:], b, &nonce, pub, pri)
return msg, nil
}
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgcfg.Key, error) {
req, err := http.NewRequest("GET", serverURL+"/key", nil)
if err != nil {
return wgcfg.Key{}, fmt.Errorf("create control key request: %v", err)
}
req = req.WithContext(ctx)
res, err := httpc.Do(req)
if err != nil {
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<16))
if err != nil {
return wgcfg.Key{}, fmt.Errorf("fetch control key response: %v", err)
}
if res.StatusCode != 200 {
return wgcfg.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
}
key, err := wgcfg.ParseHexKey(string(b))
if err != nil {
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
}
return *key, nil
}

@ -0,0 +1,305 @@
// 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 depends_on_currently_unreleased
package controlclient
import (
"context"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/klauspost/compress/zstd"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/tailcfg"
"tailscale.io/control" // not yet released
)
func TestClientsReusingKeys(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "control-test-")
if err != nil {
t.Fatal(err)
}
var server *control.Server
httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server.ServeHTTP(w, r)
}))
server, err = control.New(tmpdir, httpsrv.URL, true)
if err != nil {
t.Fatal(err)
}
server.QuietLogging = true
defer func() {
httpsrv.CloseClientConnections()
httpsrv.Close()
os.RemoveAll(tmpdir)
}()
hi := NewHostinfo()
hi.FrontendLogID = "go-test-only"
hi.BackendLogID = "go-test-only"
c1, err := NewDirect(Options{
ServerURL: httpsrv.URL,
HTTPC: httpsrv.Client(),
//TimeNow: s.control.TimeNow,
Logf: func(fmt string, args ...interface{}) {
t.Helper()
t.Logf("c1: "+fmt, args...)
},
Hostinfo: &hi,
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
authURL, err := c1.TryLogin(ctx, nil, 0)
if err != nil {
t.Fatal(err)
}
const user = "testuser1@tailscale.onmicrosoft.com"
postAuthURL(t, ctx, httpsrv.Client(), user, authURL)
newURL, err := c1.WaitLoginURL(ctx, authURL)
if err != nil {
t.Fatal(err)
}
if newURL != "" {
t.Fatalf("unexpected newURL: %s", newURL)
}
pollErrCh := make(chan error)
go func() {
err := c1.PollNetMap(ctx, -1, func(netMap *NetworkMap) {})
pollErrCh <- err
}()
select {
case err := <-pollErrCh:
t.Fatal(err)
default:
}
c2, err := NewDirect(Options{
ServerURL: httpsrv.URL,
HTTPC: httpsrv.Client(),
Logf: func(fmt string, args ...interface{}) {
t.Helper()
t.Logf("c2: "+fmt, args...)
},
Persist: c1.GetPersist(),
Hostinfo: &hi,
NewDecompressor: func() (Decompressor, error) {
return zstd.NewReader(nil)
},
KeepAlive: true,
})
if err != nil {
t.Fatal(err)
}
authURL, err = c2.TryLogin(ctx, nil, 0)
if err != nil {
t.Fatal(err)
}
if authURL != "" {
t.Errorf("unexpected authURL %s", authURL)
}
err = c2.PollNetMap(ctx, 1, func(netMap *NetworkMap) {})
if err != nil {
t.Fatal(err)
}
select {
case err := <-pollErrCh:
t.Logf("expected poll error: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("first client poll failed to close")
}
}
func TestClientsReusingOldKey(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "control-test-")
if err != nil {
t.Fatal(err)
}
var server *control.Server
httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server.ServeHTTP(w, r)
}))
server, err = control.New(tmpdir, httpsrv.URL, true)
if err != nil {
t.Fatal(err)
}
server.QuietLogging = true
defer func() {
httpsrv.CloseClientConnections()
httpsrv.Close()
os.RemoveAll(tmpdir)
}()
hi := NewHostinfo()
hi.FrontendLogID = "go-test-only"
hi.BackendLogID = "go-test-only"
genOpts := func() Options {
return Options{
ServerURL: httpsrv.URL,
HTTPC: httpsrv.Client(),
//TimeNow: s.control.TimeNow,
Logf: func(fmt string, args ...interface{}) {
t.Helper()
t.Logf("c1: "+fmt, args...)
},
Hostinfo: &hi,
}
}
// Login with a new node key. This requires authorization.
c1, err := NewDirect(genOpts())
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
authURL, err := c1.TryLogin(ctx, nil, 0)
if err != nil {
t.Fatal(err)
}
const user = "testuser1@tailscale.onmicrosoft.com"
postAuthURL(t, ctx, httpsrv.Client(), user, authURL)
newURL, err := c1.WaitLoginURL(ctx, authURL)
if err != nil {
t.Fatal(err)
}
if newURL != "" {
t.Fatalf("unexpected newURL: %s", newURL)
}
if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
t.Fatal(err)
}
newPrivKey := func(t *testing.T) wgcfg.PrivateKey {
t.Helper()
k, err := wgcfg.NewPrivateKey()
if err != nil {
t.Fatal(err)
}
return *k
}
// Replace the previous key with a new key.
persist1 := c1.GetPersist()
persist2 := Persist{
PrivateMachineKey: persist1.PrivateMachineKey,
OldPrivateNodeKey: persist1.PrivateNodeKey,
PrivateNodeKey: newPrivKey(t),
}
opts := genOpts()
opts.Persist = persist2
c1, err = NewDirect(opts)
if err != nil {
t.Fatal(err)
}
if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
t.Fatal(err)
} else if authURL == "" {
t.Fatal("expected authURL for reused oldNodeKey, got none")
} else {
postAuthURL(t, ctx, httpsrv.Client(), user, authURL)
if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
t.Fatal(err)
} else if newURL != "" {
t.Fatalf("unexpected newURL: %s", newURL)
}
}
if p := c1.GetPersist(); p.PrivateNodeKey != opts.Persist.PrivateNodeKey {
t.Error("unexpected node key change")
} else {
persist2 = p
}
// Here we simulate a client using using old persistant data.
// We use the key we have already replaced as the old node key.
// This requires the user to authenticate.
persist3 := Persist{
PrivateMachineKey: persist1.PrivateMachineKey,
OldPrivateNodeKey: persist1.PrivateNodeKey,
PrivateNodeKey: newPrivKey(t),
}
opts = genOpts()
opts.Persist = persist3
c1, err = NewDirect(opts)
if err != nil {
t.Fatal(err)
}
if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
t.Fatal(err)
} else if authURL == "" {
t.Fatal("expected authURL for reused oldNodeKey, got none")
} else {
postAuthURL(t, ctx, httpsrv.Client(), user, authURL)
if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
t.Fatal(err)
} else if newURL != "" {
t.Fatalf("unexpected newURL: %s", newURL)
}
}
if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
t.Fatal(err)
}
// At this point, there should only be one node for the machine key
// registered as active in the server.
mkey := tailcfg.MachineKey(*persist1.PrivateMachineKey.Public())
nodeIDs, err := server.DB().MachineNodes(mkey)
if err != nil {
t.Fatal(err)
}
if len(nodeIDs) != 1 {
t.Logf("active nodes for machine key %v:", mkey)
for i, nodeID := range nodeIDs {
nodeKey := server.DB().NodeKey(nodeID)
t.Logf("\tnode %d: id=%v, key=%v", i, nodeID, nodeKey)
}
t.Fatalf("want 1 active node for the client machine, got %d", len(nodeIDs))
}
// Now try the previous node key. It should fail.
opts = genOpts()
opts.Persist = persist2
c1, err = NewDirect(opts)
if err != nil {
t.Fatal(err)
}
// TODO(crawshaw): make this return an actual error.
// Have cfgdb track expired keys, and when an expired key is reused
// produce an error.
if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
t.Fatal(err)
} else if authURL == "" {
t.Fatal("expected authURL for reused nodeKey, got none")
} else {
postAuthURL(t, ctx, httpsrv.Client(), user, authURL)
if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
t.Fatal(err)
} else if newURL != "" {
t.Fatalf("unexpected newURL: %s", newURL)
}
}
if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
t.Fatal(err)
}
if nodeIDs, err := server.DB().MachineNodes(mkey); err != nil {
t.Fatal(err)
} else if len(nodeIDs) != 1 {
t.Fatalf("want 1 active node for the client machine, got %d", len(nodeIDs))
}
}

@ -0,0 +1,294 @@
// 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 controlclient
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net"
"runtime"
"strings"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/tailcfg"
"tailscale.com/wgengine/filter"
)
type NetworkMap struct {
// Core networking
NodeKey tailcfg.NodeKey
PrivateKey wgcfg.PrivateKey
Expiry time.Time
Addresses []wgcfg.CIDR
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
Peers []tailcfg.Node
DNS []wgcfg.IP
DNSDomains []string
Hostinfo tailcfg.Hostinfo
PacketFilter filter.Matches
// ACLs
User tailcfg.UserID
Domain string
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
// There are lots of ways to slice this data, leave it up to users.
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
Roles []tailcfg.Role
// TODO(crawshaw): Groups []tailcfg.Group
// TODO(crawshaw): Capabilities []tailcfg.Capability
}
func (n *NetworkMap) Equal(n2 *NetworkMap) bool {
// TODO(crawshaw): this is crude, but is an easy way to avoid bugs.
b, err := json.Marshal(n)
if err != nil {
panic(err)
}
b2, err := json.Marshal(n2)
if err != nil {
panic(err)
}
return bytes.Equal(b, b2)
}
func (n *NetworkMap) isEmpty() bool {
if n == nil {
return true
}
return n.Equal(&NetworkMap{})
}
func (nm NetworkMap) String() string {
return nm.Concise()
}
func keyString(key [32]byte) string {
b64 := base64.StdEncoding.EncodeToString(key[:])
abbrev := "invalid"
if len(b64) == 44 {
abbrev = b64[0:4] + "…" + b64[39:43]
}
return fmt.Sprintf("[%s]", abbrev)
}
func (nm *NetworkMap) Concise() string {
buf := new(strings.Builder)
fmt.Fprintf(buf, "NetworkMap: self: %v auth=%v :%v %v\n",
keyString(nm.NodeKey), nm.MachineStatus,
nm.LocalPort, nm.Addresses)
for _, p := range nm.Peers {
aip := make([]string, len(p.AllowedIPs))
for i, a := range p.AllowedIPs {
aip[i] = fmt.Sprint(a)
}
u := fmt.Sprint(p.User)
if strings.HasPrefix(u, "userid:") {
u = "u:" + u[7:]
}
f1 := fmt.Sprintf(" %v %-6v %v",
keyString(p.Key), u, p.Endpoints)
f2 := fmt.Sprintf(" %*v\n", 70-len(f1),
strings.Join(aip, " "))
fmt.Fprintf(buf, "%s%s", f1, f2)
}
return buf.String()
}
func (nm *NetworkMap) JSON() string {
b, err := json.MarshalIndent(*nm, "", " ")
if err != nil {
return fmt.Sprintf("[json error: %v]", err)
}
return string(b)
}
// TODO(apenwarr): delete me once relaynode doesn't need this anymore.
// control.go:userMap() supercedes it. This does not belong in the client.
func (nm *NetworkMap) UserMap() map[string][]filter.IP {
// Make a lookup table of roles
log.Printf("roles list is: %v\n", nm.Roles)
roles := make(map[tailcfg.RoleID]tailcfg.Role)
for _, role := range nm.Roles {
roles[role.ID] = role
}
// First, go through each node's addresses and make a lookup table
// of IP->User.
fwd := make(map[wgcfg.IP]string)
for _, node := range nm.Peers {
for _, addr := range node.Addresses {
if addr.Mask == 32 && addr.IP.Is4() {
user, ok := nm.UserProfiles[node.User]
if ok {
fwd[addr.IP] = user.LoginName
}
}
}
}
// Next, reverse the mapping into User->IP.
rev := make(map[string][]filter.IP)
for ip, username := range fwd {
ip4 := ip.To4()
if ip4 != nil {
fip := filter.NewIP(net.IP(ip4))
rev[username] = append(rev[username], fip)
}
}
// Now add roles, which are lists of users, and therefore lists
// of those users' IP addresses.
for _, user := range nm.UserProfiles {
for _, roleid := range user.Roles {
role, ok := roles[roleid]
if ok {
rolename := "role:" + role.Name
rev[rolename] = append(rev[rolename], rev[user.LoginName]...)
}
}
}
//log.Printf("Usermap is: %v\n", rev)
return rev
}
var iOS = runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64")
var keepalive = !iOS
const (
UAllowSingleHosts = 1 << iota
UAllowSubnetRoutes
UAllowDefaultRoute
UHackDefaultRoute
UDefault = 0
)
// Several programs need to parse these arguments into uflags, so let's
// centralize it here.
func UFlagsHelper(uroutes, rroutes, droutes bool) int {
uflags := 0
if uroutes {
uflags |= UAllowSingleHosts
}
if rroutes {
uflags |= UAllowSubnetRoutes
}
if droutes {
uflags |= UAllowDefaultRoute
}
return uflags
}
func (nm *NetworkMap) UAPI(uflags int, dnsOverride []wgcfg.IP) string {
wgcfg, err := nm.WGCfg(uflags, dnsOverride)
if err != nil {
log.Fatalf("WGCfg() failed unexpectedly: %v\n", err)
}
s, err := wgcfg.ToUAPI()
if err != nil {
log.Fatalf("ToUAPI() failed unexpectedly: %v\n", err)
}
return s
}
func (nm *NetworkMap) WGCfg(uflags int, dnsOverride []wgcfg.IP) (*wgcfg.Config, error) {
s := nm._WireGuardConfig(uflags, dnsOverride, true)
return wgcfg.FromWgQuick(s, "tailscale")
}
// TODO(apenwarr): This mode is dangerous.
// Discarding the extra endpoints is almost universally the wrong choice.
// Except that plain wireguard can't handle a peer with multiple endpoints.
// (Yet?)
func (nm *NetworkMap) WireGuardConfigOneEndpoint(uflags int, dnsOverride []wgcfg.IP) string {
return nm._WireGuardConfig(uflags, dnsOverride, false)
}
func (nm *NetworkMap) _WireGuardConfig(uflags int, dnsOverride []wgcfg.IP, allEndpoints bool) string {
buf := new(strings.Builder)
fmt.Fprintf(buf, "[Interface]\n")
fmt.Fprintf(buf, "PrivateKey = %s\n", base64.StdEncoding.EncodeToString(nm.PrivateKey[:]))
if len(nm.Addresses) > 0 {
fmt.Fprintf(buf, "Address = ")
for i, cidr := range nm.Addresses {
if i > 0 {
fmt.Fprintf(buf, ", ")
}
fmt.Fprintf(buf, "%s", cidr)
}
fmt.Fprintf(buf, "\n")
}
fmt.Fprintf(buf, "ListenPort = %d\n", nm.LocalPort)
if len(dnsOverride) > 0 {
dnss := []string{}
for _, ip := range dnsOverride {
dnss = append(dnss, ip.String())
}
fmt.Fprintf(buf, "DNS = %s\n", strings.Join(dnss, ","))
}
fmt.Fprintf(buf, "\n")
for i, peer := range nm.Peers {
if (uflags&UAllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
log.Printf("wgcfg: %v skipping a single-host peer.\n", peer.Key.AbbrevString())
continue
}
if i > 0 {
fmt.Fprintf(buf, "\n")
}
fmt.Fprintf(buf, "[Peer]\n")
fmt.Fprintf(buf, "PublicKey = %s\n", base64.StdEncoding.EncodeToString(peer.Key[:]))
if len(peer.Endpoints) > 0 {
if len(peer.Endpoints) == 1 {
fmt.Fprintf(buf, "Endpoint = %s", peer.Endpoints[0])
} else if allEndpoints {
// TODO(apenwarr): This mode is incompatible.
// Normal wireguard clients don't know how to
// parse it (yet?)
fmt.Fprintf(buf, "Endpoint = %s",
strings.Join(peer.Endpoints, ","))
} else {
fmt.Fprintf(buf, "Endpoint = %s # other endpoints: %s",
peer.Endpoints[0],
strings.Join(peer.Endpoints[1:], ", "))
}
buf.WriteByte('\n')
}
var aips []string
for _, allowedIP := range peer.AllowedIPs {
aip := allowedIP.String()
if allowedIP.Mask == 0 {
if (uflags & UAllowDefaultRoute) == 0 {
log.Printf("wgcfg: %v skipping default route\n", peer.Key.AbbrevString())
continue
}
if (uflags & UHackDefaultRoute) != 0 {
aip = "10.0.0.0/8"
log.Printf("wgcfg: %v converting default route => %v\n", peer.Key.AbbrevString(), aip)
}
} else if allowedIP.Mask < 32 {
if (uflags & UAllowSubnetRoutes) == 0 {
log.Printf("wgcfg: %v skipping subnet route\n", peer.Key.AbbrevString())
continue
}
}
aips = append(aips, aip)
}
fmt.Fprintf(buf, "AllowedIPs = %s\n", strings.Join(aips, ", "))
if keepalive {
fmt.Fprintf(buf, "PersistentKeepalive = 25\n")
}
}
return buf.String()
}

@ -0,0 +1,227 @@
// 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 policy
import (
"bytes"
"errors"
"fmt"
"github.com/tailscale/hujson"
"net"
"strconv"
"strings"
"tailscale.com/wgengine/filter"
)
type IP = filter.IP
const IPAny = filter.IPAny
type row struct {
Action string
Users []string
Ports []string
}
type Policy struct {
ACLs []row
Groups map[string][]string
Hosts map[string]IP
}
func lineAndColumn(b []byte, ofs int64) (line, col int) {
line = 1
for _, c := range b[:ofs] {
if c == '\n' {
col = 1
line++
} else {
col++
}
}
return line, col
}
func betterUnmarshal(b []byte, obj interface{}) error {
bio := bytes.NewReader(b)
d := hujson.NewDecoder(bio)
d.DisallowUnknownFields()
err := d.Decode(obj)
if err != nil {
switch ee := err.(type) {
case *hujson.SyntaxError:
row, col := lineAndColumn(b, ee.Offset)
return fmt.Errorf("line %d col %d: %v", row, col, ee)
default:
return fmt.Errorf("parser: %v", err)
}
}
return nil
}
func Parse(acljson string) (*Policy, error) {
p := &Policy{}
err := betterUnmarshal([]byte(acljson), p)
if err != nil {
return nil, err
}
// Check syntax with an empty usermap to start with.
// The caller might not have a valid usermap at startup, but we still
// want to check that the acljson doesn't have any syntax errors
// as early as possible. When the usermap updates later, it won't
// add any new syntax errors.
//
// TODO(apenwarr): change unmarshal code to detect syntax errors above.
// Right now some of the sub-objects aren't parsed until .Expand().
emptyUserMap := make(map[string][]IP)
_, err = p.Expand(emptyUserMap)
if err != nil {
return nil, err
}
return p, nil
}
func parseHostPortRange(hostport string) (host string, ports []filter.PortRange, err error) {
hl := strings.Split(hostport, ":")
if len(hl) != 2 {
return "", nil, errors.New("hostport must have exactly one colon(:)")
}
host = hl[0]
portlist := hl[1]
if portlist == "*" {
// Special case: permit hostname:* as a port wildcard.
ports = append(ports, filter.PortRangeAny)
return host, ports, nil
}
pl := strings.Split(portlist, ",")
for _, pp := range pl {
if len(pp) == 0 {
return "", nil, fmt.Errorf("invalid port list: %#v", portlist)
}
pr := strings.Split(pp, "-")
if len(pr) > 2 {
return "", nil, fmt.Errorf("port range %#v: too many dashes(-)", pp)
}
var first, last uint64
first, err := strconv.ParseUint(pr[0], 10, 16)
if err != nil {
return "", nil, fmt.Errorf("port range %#v: invalid first integer", pp)
}
if len(pr) >= 2 {
last, err = strconv.ParseUint(pr[1], 10, 16)
if err != nil {
return "", nil, fmt.Errorf("port range %#v: invalid last integer", pp)
}
} else {
last = first
}
if first == 0 {
return "", nil, fmt.Errorf("port range %#v: first port must be >0, or use '*' for wildcard", pp)
}
if first > last {
return "", nil, fmt.Errorf("port range %#v: first port must be >= last port", pp)
}
ports = append(ports, filter.PortRange{uint16(first), uint16(last)})
}
return host, ports, nil
}
func (p *Policy) Expand(usermap map[string][]IP) (filter.Matches, error) {
lcusermap := make(map[string][]IP)
for k, v := range usermap {
k = strings.ToLower(k)
lcusermap[k] = v
}
for k, userlist := range p.Groups {
k = strings.ToLower(k)
if !strings.HasPrefix(k, "group:") {
return nil, fmt.Errorf("Group[%#v]: group names must start with 'group:'", k)
}
for _, u := range userlist {
uips := lcusermap[u]
lcusermap[k] = append(lcusermap[k], uips...)
}
}
hosts := p.Hosts
var out filter.Matches
for _, acl := range p.ACLs {
if acl.Action != "accept" {
return nil, fmt.Errorf("Action=%#v is not supported", acl.Action)
}
var srcs []IP
for _, user := range acl.Users {
user = strings.ToLower(user)
if user == "*" {
srcs = append(srcs, IPAny)
continue
} else if strings.Contains(user, "@") ||
strings.HasPrefix(user, "role:") ||
strings.HasPrefix(user, "group:") {
// fine if the requested user doesn't exist.
// we don't want to crash ACL parsing just
// because a previously authed user gets
// deleted. We'll silently ignore it and
// no firewall rules are needed.
// TODO(apenwarr): maybe print a warning?
for _, ip := range lcusermap[user] {
if ip != IPAny {
srcs = append(srcs, ip)
}
}
} else {
return nil, fmt.Errorf("wgengine/filter: invalid username: %q: needs @domain or group: or role:", user)
}
}
var dsts []filter.IPPortRange
for _, hostport := range acl.Ports {
host, ports, err := parseHostPortRange(hostport)
if err != nil {
return nil, fmt.Errorf("Ports=%#v: %v", hostport, err)
}
ip := net.ParseIP(host)
ipv, ok := hosts[host]
if ok {
// matches an alias; ipv is now valid
} else if ip != nil && ip.IsUnspecified() {
// For clarity, reject 0.0.0.0 as an input
return nil, fmt.Errorf("Ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", hostport)
} else if ip == nil && host == "*" {
// User explicitly requested wildcard dst ip
ipv = IPAny
} else {
if ip != nil {
ip = ip.To4()
}
if ip == nil || len(ip) != 4 {
return nil, fmt.Errorf("Ports=%#v: %#v: invalid IPv4 address", hostport, host)
}
ipv = filter.NewIP(ip)
}
for _, pr := range ports {
dsts = append(dsts, filter.IPPortRange{ipv, pr})
}
}
out = append(out, filter.Match{DstPorts: dsts, SrcIPs: srcs})
}
return out, nil
}

@ -0,0 +1,156 @@
// 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 policy
import (
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/wgengine/filter"
)
type PortRange = filter.PortRange
type IPPortRange = filter.IPPortRange
var syntax_errors = []string{
`{ "ACLs": []! }`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "xPorts": ["100.122.98.50:22"]}
]}`,
`{ "ACLs": [
{"Action": "drop", "Users": [], "Ports": ["100.122.98.50:22"]}
]}`,
`{ "ACLs": [
{"Users": [], "Ports": ["100.122.98.50:22"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4:0"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["0.0.0.0:12"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["*:0"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4:5:6"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4.5:12"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4::12"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4:0-0"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4:1-10,2-"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4:1-10,*"]}
]}`,
`{ "ACLs": [
{"Action": "accept", "Users": [], "Ports": ["1.2.3.4,5.6.7.8:1-10"]}
]}`,
`{ "Hosts": {"mailserver": "not-an-ip"} }`,
`{ "Hosts": {"mailserver": "1.2.3.4:55"} }`,
`{ "xGroups": {
"bob": ["user1", "user2"]
}}`,
}
func TestSyntaxErrors(t *testing.T) {
for _, s := range syntax_errors {
_, err := Parse(s)
if err == nil {
t.Fatalf("Parse passed when it shouldn't. json:\n---\n%v\n---", s)
}
}
}
func ippr(ip IP, start, end uint16) []IPPortRange {
return []IPPortRange{
IPPortRange{ip, PortRange{start, end}},
}
}
func TestPolicy(t *testing.T) {
// Check ACL table parsing
usermap := map[string][]IP{
"A@b.com": []IP{0x08010101, 0x08020202},
"role:admin": []IP{0x02020202},
"user1@org": []IP{0x99010101, 0x99010102},
// user2 is intentionally missing
"user3@org": []IP{0x99030303},
"user4@org": []IP{},
}
want := filter.Matches{
{SrcIPs: []IP{0x08010101, 0x08020202}, DstPorts: []IPPortRange{
IPPortRange{0x01020304, PortRange{22, 22}},
IPPortRange{0x05060708, PortRange{23, 24}},
IPPortRange{0x05060708, PortRange{27, 28}},
}},
{SrcIPs: []IP{0x02020202}, DstPorts: ippr(0x08010101, 22, 22)},
{SrcIPs: []IP{0}, DstPorts: []IPPortRange{
IPPortRange{0x647a6232, PortRange{0, 65535}},
IPPortRange{0, PortRange{443, 443}},
}},
{SrcIPs: []IP{0x99010101, 0x99010102, 0x99030303}, DstPorts: ippr(0x01020304, 999, 999)},
}
p, err := Parse(`
{
// Test comment
"Hosts": {
"h1": "1.2.3.4", /* test comment */
"h2": "5.6.7.8"
},
"Groups": {
"group:eng": ["user1@org", "user2@org", "user3@org", "user4@org"]
},
"ACLs": [
{"Action": "accept", "Users": ["a@b.com"], "Ports": ["h1:22", "h2:23-24,27-28"]},
{"Action": "accept", "Users": ["role:Admin"], "Ports": ["8.1.1.1:22"]},
{"Action": "accept", "Users": ["*"], "Ports": ["100.122.98.50:*", "*:443"]},
{"Action": "accept", "Users": ["group:eng"], "Ports": ["h1:999"]},
]}
`)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
matches, err := p.Expand(usermap)
if err != nil {
t.Fatalf("Expand failed: %v", err)
}
if diff := cmp.Diff(want, matches); diff != "" {
t.Fatalf("Expand mismatch (-want +got):\n%s", diff)
}
}

@ -0,0 +1,182 @@
// 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 derp
import (
"bufio"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net"
"time"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
)
type Client struct {
serverKey [32]byte
privateKey [32]byte // TODO(crawshaw): make this wgcfg.PrivateKey?
publicKey [32]byte
logf func(format string, args ...interface{})
netConn net.Conn
conn *bufio.ReadWriter
}
func NewClient(privateKey [32]byte, netConn net.Conn, conn *bufio.ReadWriter, logf func(format string, args ...interface{})) (*Client, error) {
c := &Client{
privateKey: privateKey,
logf: logf,
netConn: netConn,
conn: conn,
}
curve25519.ScalarBaseMult(&c.publicKey, &c.privateKey)
if err := c.recvServerKey(); err != nil {
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
}
if err := c.sendClientKey(); err != nil {
return nil, fmt.Errorf("derp.Client: failed to send client key: %v", err)
}
_, err := c.recvServerInfo()
if err != nil {
return nil, fmt.Errorf("derp.Client: failed to receive server info: %v", err)
}
return c, nil
}
func (c *Client) recvServerKey() error {
gotMagic, err := readUint32(c.conn, 0xffffffff)
if err != nil {
return err
}
if gotMagic != magic {
return fmt.Errorf("bad magic %x, want %x", gotMagic, magic)
}
if err := readType(c.conn.Reader, typeServerKey); err != nil {
return err
}
if _, err := io.ReadFull(c.conn, c.serverKey[:]); err != nil {
return err
}
return nil
}
func (c *Client) recvServerInfo() (*serverInfo, error) {
if err := readType(c.conn.Reader, typeServerInfo); err != nil {
return nil, err
}
var nonce [24]byte
if _, err := io.ReadFull(c.conn, nonce[:]); err != nil {
return nil, fmt.Errorf("nonce: %v", err)
}
msgLen, err := readUint32(c.conn, oneMB)
if err != nil {
return nil, fmt.Errorf("msglen: %v", err)
}
msgbox := make([]byte, msgLen)
if _, err := io.ReadFull(c.conn, msgbox); err != nil {
return nil, fmt.Errorf("msgbox: %v", err)
}
msg, ok := box.Open(nil, msgbox, &nonce, &c.serverKey, &c.privateKey)
if !ok {
return nil, fmt.Errorf("msgbox: cannot open len=%d with server key %x", msgLen, c.serverKey[:])
}
info := new(serverInfo)
if err := json.Unmarshal(msg, info); err != nil {
return nil, fmt.Errorf("msg: %v", err)
}
return info, nil
}
func (c *Client) sendClientKey() error {
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
return err
}
msg := []byte("{}") // no clientInfo for now
msgbox := box.Seal(nil, msg, &nonce, &c.serverKey, &c.privateKey)
if _, err := c.conn.Write(c.publicKey[:]); err != nil {
return err
}
if _, err := c.conn.Write(nonce[:]); err != nil {
return err
}
if err := putUint32(c.conn.Writer, uint32(len(msgbox))); err != nil {
return err
}
if _, err := c.conn.Write(msgbox); err != nil {
return err
}
return c.conn.Flush()
}
func (c *Client) Send(dstKey [32]byte, msg []byte) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("derp.Send: %v", err)
}
}()
if err := c.conn.WriteByte(typeSendPacket); err != nil {
return err
}
if _, err := c.conn.Write(dstKey[:]); err != nil {
return err
}
msgLen := uint32(len(msg))
if int(msgLen) != len(msg) {
return fmt.Errorf("packet too big: %d", len(msg))
}
if err := putUint32(c.conn.Writer, msgLen); err != nil {
return err
}
if _, err := c.conn.Write(msg); err != nil {
return err
}
return c.conn.Flush()
}
func (c *Client) Recv(b []byte) (n int, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("derp.Recv: %v", err)
}
}()
loop:
for {
c.netConn.SetReadDeadline(time.Now().Add(120 * time.Second))
packetType, err := c.conn.ReadByte()
if err != nil {
return 0, err
}
switch packetType {
case typeKeepAlive:
continue
case typeRecvPacket:
break loop
default:
return 0, fmt.Errorf("derp.Recv: unknown packet type %d", packetType)
}
}
packetLen, err := readUint32(c.conn.Reader, oneMB)
if err != nil {
return 0, err
}
if int(packetLen) > len(b) {
// TODO(crawshaw): discard the packet
return 0, io.ErrShortBuffer
}
b = b[:packetLen]
if _, err := io.ReadFull(c.conn, b); err != nil {
return 0, err
}
return int(packetLen), nil
}

@ -0,0 +1,380 @@
// 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 derp
// TODO(crawshaw): revise protocol so unknown type packets have a predictable length for skipping.
// TODO(crawshaw): send srcKey with packets to clients?
// TODO(crawshaw): with predefined serverKey in clients and HMAC on packets we could skip TLS
import (
"bufio"
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"math/big"
"net"
"sync"
"time"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
)
const magic = 0x44c55250 // "DERP" with a non-ASCII high-bit
const (
typeServerKey = 0x01
typeServerInfo = 0x02
typeSendPacket = 0x03
typeRecvPacket = 0x04
typeKeepAlive = 0x05
)
const keepAlive = 60 * time.Second
var bin = binary.BigEndian
const oneMB = 1 << 20
type Server struct {
privateKey [32]byte // TODO(crawshaw): make this wgcfg.PrivateKey?
publicKey [32]byte
logf func(format string, args ...interface{})
mu sync.Mutex
netConns map[net.Conn]chan struct{}
clients map[[32]byte]*client
}
func NewServer(privateKey [32]byte, logf func(format string, args ...interface{})) *Server {
s := &Server{
privateKey: privateKey,
logf: logf,
clients: make(map[[32]byte]*client),
netConns: make(map[net.Conn]chan struct{}),
}
curve25519.ScalarBaseMult(&s.publicKey, &s.privateKey)
return s
}
func (s *Server) Close() error {
var closedChs []chan struct{}
s.mu.Lock()
for netConn, closed := range s.netConns {
netConn.Close()
closedChs = append(closedChs, closed)
}
s.mu.Unlock()
for _, closed := range closedChs {
<-closed
}
return nil
}
func (s *Server) Accept(netConn net.Conn, conn *bufio.ReadWriter) {
closed := make(chan struct{})
s.mu.Lock()
s.netConns[netConn] = closed
s.mu.Unlock()
defer func() {
netConn.Close()
close(closed)
s.mu.Lock()
delete(s.netConns, netConn)
s.mu.Unlock()
}()
if err := s.accept(netConn, conn); err != nil {
s.logf("derp: %s: %v", netConn.RemoteAddr(), err)
}
}
func (s *Server) accept(netConn net.Conn, conn *bufio.ReadWriter) error {
netConn.SetDeadline(time.Now().Add(10 * time.Second))
if err := s.sendServerKey(conn); err != nil {
return fmt.Errorf("send server key: %v", err)
}
netConn.SetDeadline(time.Now().Add(10 * time.Second))
clientKey, clientInfo, err := s.recvClientKey(conn)
if err != nil {
return fmt.Errorf("receive client key: %v", err)
}
if err := s.verifyClient(clientKey, clientInfo); err != nil {
return fmt.Errorf("client %x rejected: %v", clientKey, err)
}
// At this point we trust the client so we don't time out.
netConn.SetDeadline(time.Time{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := &client{
key: clientKey,
netConn: netConn,
conn: conn,
}
if clientInfo != nil {
c.info = *clientInfo
}
go func() {
if err := c.keepAlive(ctx); err != nil {
s.logf("derp: %s: client %x: keep alive failed: %v", netConn.RemoteAddr(), c.key, err)
}
}()
defer func() {
s.mu.Lock()
curClient := s.clients[c.key]
if curClient != nil && curClient.conn == conn {
s.logf("derp: %s: client %x: removing connection", netConn.RemoteAddr(), c.key)
delete(s.clients, c.key)
}
s.mu.Unlock()
}()
// Hold mu while we add the new client to the clients list and under
// the same acquisition send server info. This ensure that both:
// 1. by the time the client receives the server info, it can be addressed.
// 2. the server info is the very first
c.mu.Lock()
s.mu.Lock()
oldClient := s.clients[c.key]
s.clients[c.key] = c
s.mu.Unlock()
if err := s.sendServerInfo(conn, clientKey); err != nil {
return fmt.Errorf("send server info: %v", err)
}
c.mu.Unlock()
if oldClient == nil {
s.logf("derp: %s: client %x: adding connection", netConn.RemoteAddr(), c.key)
} else {
oldClient.netConn.Close()
s.logf("derp: %s: client %x: adding connection, replacing %s", netConn.RemoteAddr(), c.key, oldClient.netConn.RemoteAddr())
}
for {
dstKey, contents, err := s.recvPacket(c.conn)
if err != nil {
return fmt.Errorf("client %x: recv: %v", c.key, err)
}
s.mu.Lock()
dst := s.clients[dstKey]
s.mu.Unlock()
if dst == nil {
s.logf("derp: %s: client %x: dropping packet for unknown %x", netConn.RemoteAddr(), c.key, dstKey)
continue
}
dst.mu.Lock()
err = s.sendPacket(dst.conn, c.key, contents)
dst.mu.Unlock()
if err != nil {
s.logf("derp: %s: client %x: dropping packet for %x: %v", netConn.RemoteAddr(), c.key, dstKey, err)
// If we cannot send to a destination, shut it down.
// Let its receive loop do the cleanup.
s.mu.Lock()
if s.clients[dstKey].conn == dst.conn {
s.clients[dstKey].netConn.Close()
}
s.mu.Unlock()
}
}
}
func (s *Server) verifyClient(clientKey [32]byte, info *clientInfo) error {
// TODO(crawshaw): implement policy constraints on who can use the DERP server
return nil
}
func (s *Server) sendServerKey(conn *bufio.ReadWriter) error {
if err := putUint32(conn, magic); err != nil {
return err
}
if err := conn.WriteByte(typeServerKey); err != nil {
return err
}
if _, err := conn.Write(s.publicKey[:]); err != nil {
return err
}
return conn.Flush()
}
func (s *Server) sendServerInfo(conn *bufio.ReadWriter, clientKey [32]byte) error {
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
return err
}
msg := []byte("{}") // no serverInfo for now
msgbox := box.Seal(nil, msg, &nonce, &clientKey, &s.privateKey)
if err := conn.WriteByte(typeServerInfo); err != nil {
return err
}
if _, err := conn.Write(nonce[:]); err != nil {
return err
}
if err := putUint32(conn, uint32(len(msgbox))); err != nil {
return err
}
if _, err := conn.Write(msgbox); err != nil {
return err
}
return conn.Flush()
}
func (s *Server) recvClientKey(conn *bufio.ReadWriter) (clientKey [32]byte, info *clientInfo, err error) {
if _, err := io.ReadFull(conn, clientKey[:]); err != nil {
return [32]byte{}, nil, err
}
var nonce [24]byte
if _, err := io.ReadFull(conn, nonce[:]); err != nil {
return [32]byte{}, nil, fmt.Errorf("nonce: %v", err)
}
msgLen, err := readUint32(conn, oneMB)
if err != nil {
return [32]byte{}, nil, fmt.Errorf("msglen: %v", err)
}
msgbox := make([]byte, msgLen)
if _, err := io.ReadFull(conn, msgbox); err != nil {
return [32]byte{}, nil, fmt.Errorf("msgbox: %v", err)
}
msg, ok := box.Open(nil, msgbox, &nonce, &clientKey, &s.privateKey)
if !ok {
return [32]byte{}, nil, fmt.Errorf("msgbox: cannot open len=%d with client key %x", msgLen, clientKey[:])
}
info = new(clientInfo)
if err := json.Unmarshal(msg, info); err != nil {
return [32]byte{}, nil, fmt.Errorf("msg: %v", err)
}
return clientKey, info, nil
}
func (s *Server) sendPacket(conn *bufio.ReadWriter, srcKey [32]byte, contents []byte) error {
if err := conn.WriteByte(typeRecvPacket); err != nil {
return err
}
if err := putUint32(conn.Writer, uint32(len(contents))); err != nil {
return err
}
if _, err := conn.Write(contents); err != nil {
return err
}
return conn.Flush()
}
func (s *Server) recvPacket(conn *bufio.ReadWriter) (dstKey [32]byte, contents []byte, err error) {
if err := readType(conn.Reader, typeSendPacket); err != nil {
return [32]byte{}, nil, err
}
if _, err := io.ReadFull(conn, dstKey[:]); err != nil {
return [32]byte{}, nil, err
}
packetLen, err := readUint32(conn.Reader, oneMB)
if err != nil {
return [32]byte{}, nil, err
}
contents = make([]byte, packetLen)
if _, err := io.ReadFull(conn, contents); err != nil {
return [32]byte{}, nil, err
}
return dstKey, contents, nil
}
type client struct {
netConn net.Conn
key [32]byte
info clientInfo
keepAliveTimer *time.Timer
keepAliveReset chan struct{}
mu sync.Mutex
conn *bufio.ReadWriter
}
func (c *client) keepAlive(ctx context.Context) error {
jitterMs, err := rand.Int(rand.Reader, big.NewInt(5000))
if err != nil {
panic(err)
}
jitter := time.Duration(jitterMs.Int64()) * time.Millisecond
c.keepAliveTimer = time.NewTimer(keepAlive + jitter)
for {
select {
case <-ctx.Done():
return nil
case <-c.keepAliveReset:
if c.keepAliveTimer.Stop() {
<-c.keepAliveTimer.C
}
c.keepAliveTimer.Reset(keepAlive + jitter)
case <-c.keepAliveTimer.C:
c.mu.Lock()
err := c.conn.WriteByte(typeKeepAlive)
if err == nil {
err = c.conn.Flush()
}
c.mu.Unlock()
if err != nil {
// TODO log
c.netConn.Close()
return err
}
}
}
}
type clientInfo struct {
}
type serverInfo struct {
}
func readType(r *bufio.Reader, t uint8) error {
packetType, err := r.ReadByte()
if err != nil {
return err
}
if packetType != t {
return fmt.Errorf("bad packet type 0x%X, want 0x%X", packetType, t)
}
return nil
}
func putUint32(w io.Writer, v uint32) error {
var b [4]byte
bin.PutUint32(b[:], v)
_, err := w.Write(b[:])
return err
}
func readUint32(r io.Reader, maxVal uint32) (uint32, error) {
b := make([]byte, 4)
if _, err := io.ReadFull(r, b); err != nil {
return 0, err
}
val := bin.Uint32(b)
if val > maxVal {
return 0, fmt.Errorf("uint32 %d exceeds limit %d", val, maxVal)
}
return val, nil
}

@ -0,0 +1,125 @@
// 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 derp
import (
"bufio"
"crypto/rand"
"net"
"testing"
"time"
"golang.org/x/crypto/curve25519"
)
func TestSendRecv(t *testing.T) {
const numClients = 3
var serverPrivateKey [32]byte
if _, err := rand.Read(serverPrivateKey[:]); err != nil {
t.Fatal(err)
}
var clientPrivateKeys [][32]byte
for i := 0; i < numClients; i++ {
var key [32]byte
if _, err := rand.Read(key[:]); err != nil {
t.Fatal(err)
}
clientPrivateKeys = append(clientPrivateKeys, key)
}
var clientKeys [][32]byte
for _, privKey := range clientPrivateKeys {
var key [32]byte
curve25519.ScalarBaseMult(&key, &privKey)
clientKeys = append(clientKeys, key)
}
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
var clientConns []net.Conn
for i := 0; i < numClients; i++ {
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
clientConns = append(clientConns, conn)
}
s := NewServer(serverPrivateKey, t.Logf)
defer s.Close()
for i := 0; i < numClients; i++ {
netConn, err := ln.Accept()
if err != nil {
t.Fatal(err)
}
conn := bufio.NewReadWriter(bufio.NewReader(netConn), bufio.NewWriter(netConn))
go s.Accept(netConn, conn)
}
var clients []*Client
var recvChs []chan []byte
errCh := make(chan error, 3)
for i := 0; i < numClients; i++ {
key := clientPrivateKeys[i]
netConn := clientConns[i]
conn := bufio.NewReadWriter(bufio.NewReader(netConn), bufio.NewWriter(netConn))
c, err := NewClient(key, netConn, conn, t.Logf)
if err != nil {
t.Fatalf("client %d: %v", i, err)
}
clients = append(clients, c)
recvChs = append(recvChs, make(chan []byte))
go func(i int) {
for {
b := make([]byte, 1<<16)
n, err := c.Recv(b)
if err != nil {
errCh <- err
return
}
b = b[:n]
recvChs[i] <- b
}
}(i)
}
recv := func(i int, want string) {
t.Helper()
select {
case b := <-recvChs[i]:
if got := string(b); got != want {
t.Errorf("client1.Recv=%q, want %q", got, want)
}
case <-time.After(1 * time.Second):
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
}
}
recvNothing := func(i int) {
t.Helper()
select {
case b := <-recvChs[0]:
t.Errorf("client%d.Recv=%q, want nothing", i, string(b))
default:
}
}
msg1 := []byte("hello 0->1\n")
if err := clients[0].Send(clientKeys[1], msg1); err != nil {
t.Fatal(err)
}
recv(1, string(msg1))
recvNothing(0)
recvNothing(2)
msg2 := []byte("hello 1->2\n")
if err := clients[1].Send(clientKeys[2], msg2); err != nil {
t.Fatal(err)
}
recv(2, string(msg2))
recvNothing(0)
recvNothing(1)
}

@ -0,0 +1,203 @@
// 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 derphttp implements DERP-over-HTTP.
//
// This makes DERP look exactly like WebSockets.
// A server can implement DERP over HTTPS and even if the TLS connection
// intercepted using a fake root CA, unless the interceptor knows how to
// detect DERP packets, it will look like a web socket.
package derphttp
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"sync"
"tailscale.com/derp"
)
// Client is a DERP-over-HTTP client.
//
// It automatically reconnects on error retry. That is, a failed Send or
// Recv will report the error and not retry, but subsequent calls to
// Send/Recv will completely re-establish the connection.
type Client struct {
privateKey [32]byte
logf func(format string, args ...interface{})
closed chan struct{}
url *url.URL
resp *http.Response
netConnMu sync.Mutex
netConn net.Conn
clientMu sync.Mutex
client *derp.Client
}
func NewClient(privateKey [32]byte, serverURL string, logf func(format string, args ...interface{})) (c *Client, err error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, fmt.Errorf("derphttp.NewClient: %v", err)
}
c = &Client{
privateKey: privateKey,
logf: logf,
url: u,
closed: make(chan struct{}),
}
if _, err := c.connect("derphttp.NewClient"); err != nil {
c.logf("%v", err)
}
return c, nil
}
func (c *Client) connect(caller string) (client *derp.Client, err error) {
select {
case <-c.closed:
return nil, ErrClientClosed
default:
}
c.clientMu.Lock()
defer c.clientMu.Unlock()
if c.client != nil {
return c.client, nil
}
c.logf("%s: connecting", caller)
var netConn net.Conn
defer func() {
if err != nil {
err = fmt.Errorf("%s connect: %v", caller, err)
if netConn := netConn; netConn != nil {
netConn.Close()
}
}
}()
if c.url.Scheme == "https" {
port := c.url.Port()
if port == "" {
port = "443"
}
config := &tls.Config{}
var tlsConn *tls.Conn
tlsConn, err = tls.Dial("tcp", net.JoinHostPort(c.url.Host, port), config)
if tlsConn != nil {
netConn = tlsConn
}
} else {
netConn, err = net.Dial("tcp", c.url.Host)
}
if err != nil {
return nil, err
}
c.netConnMu.Lock()
c.netConn = netConn
c.netConnMu.Unlock()
conn := bufio.NewReadWriter(bufio.NewReader(netConn), bufio.NewWriter(netConn))
req, err := http.NewRequest("GET", c.url.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Upgrade", "WebSocket")
req.Header.Set("Connection", "Upgrade")
if err := req.Write(conn); err != nil {
return nil, err
}
if err := conn.Flush(); err != nil {
return nil, err
}
resp, err := http.ReadResponse(conn.Reader, req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
b, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("GET failed: %v: %s", err, b)
}
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
derpClient, err := derp.NewClient(c.privateKey, netConn, conn, c.logf)
if err != nil {
return nil, err
}
c.resp = resp
c.client = derpClient
return c.client, nil
}
func (c *Client) Send(dstKey [32]byte, b []byte) error {
client, err := c.connect("derphttp.Client.Send")
if err != nil {
return err
}
if err := client.Send(dstKey, b); err != nil {
c.close()
}
return err
}
func (c *Client) Recv(b []byte) (int, error) {
client, err := c.connect("derphttp.Client.Recv")
if err != nil {
return 0, err
}
n, err := client.Recv(b)
if err != nil {
c.close()
}
return n, err
}
func (c *Client) Close() error {
select {
case <-c.closed:
return ErrClientClosed
default:
}
close(c.closed)
c.close()
return nil
}
func (c *Client) close() {
c.netConnMu.Lock()
netConn := c.netConn
c.netConnMu.Unlock()
if netConn != nil {
netConn.Close()
}
c.clientMu.Lock()
defer c.clientMu.Unlock()
if c.client == nil {
return
}
c.resp = nil
c.client = nil
c.netConnMu.Lock()
c.netConn = nil
c.netConnMu.Unlock()
}
var ErrClientClosed = errors.New("derphttp.Client closed")

@ -0,0 +1,35 @@
// 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 derphttp
import (
"net/http"
"tailscale.com/derp"
)
func Handler(s *derp.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") != "WebSocket" {
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
return
}
w.Header().Set("Upgrade", "WebSocket")
w.Header().Set("Connection", "Upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
h, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "HTTP does not support general TCP support", 500)
return
}
netConn, conn, err := h.Hijack()
if err != nil {
http.Error(w, "HTTP does not support general TCP support", 500)
return
}
s.Accept(netConn, conn)
})
}

@ -0,0 +1,142 @@
// 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 derphttp
import (
"crypto/rand"
"crypto/tls"
"net"
"net/http"
"sync"
"testing"
"time"
"golang.org/x/crypto/curve25519"
"tailscale.com/derp"
)
func TestSendRecv(t *testing.T) {
const numClients = 3
var serverPrivateKey [32]byte
if _, err := rand.Read(serverPrivateKey[:]); err != nil {
t.Fatal(err)
}
var clientPrivateKeys [][32]byte
for i := 0; i < numClients; i++ {
var key [32]byte
if _, err := rand.Read(key[:]); err != nil {
t.Fatal(err)
}
clientPrivateKeys = append(clientPrivateKeys, key)
}
var clientKeys [][32]byte
for _, privKey := range clientPrivateKeys {
var key [32]byte
curve25519.ScalarBaseMult(&key, &privKey)
clientKeys = append(clientKeys, key)
}
s := derp.NewServer(serverPrivateKey, t.Logf)
defer s.Close()
httpsrv := &http.Server{
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: Handler(s),
}
ln, err := net.Listen("tcp4", "localhost:0")
if err != nil {
t.Fatal(err)
}
serverURL := "http://" + ln.Addr().String()
t.Logf("server URL: %s", serverURL)
go func() {
if err := httpsrv.Serve(ln); err != nil {
if err == http.ErrServerClosed {
return
}
panic(err)
}
}()
var clients []*Client
var recvChs []chan []byte
done := make(chan struct{})
var wg sync.WaitGroup
defer func() {
close(done)
for _, c := range clients {
c.Close()
}
wg.Wait()
}()
for i := 0; i < numClients; i++ {
key := clientPrivateKeys[i]
c, err := NewClient(key, serverURL, t.Logf)
if err != nil {
t.Fatalf("client %d: %v", i, err)
}
clients = append(clients, c)
recvChs = append(recvChs, make(chan []byte))
wg.Add(1)
go func(i int) {
defer wg.Done()
for {
select {
case <-done:
return
default:
}
b := make([]byte, 1<<16)
n, err := c.Recv(b)
if err != nil {
t.Logf("client%d: %v", i, err)
break
}
b = b[:n]
recvChs[i] <- b
}
}(i)
}
recv := func(i int, want string) {
t.Helper()
select {
case b := <-recvChs[i]:
if got := string(b); got != want {
t.Errorf("client1.Recv=%q, want %q", got, want)
}
case <-time.After(1 * time.Second):
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
}
}
recvNothing := func(i int) {
t.Helper()
select {
case b := <-recvChs[0]:
t.Errorf("client%d.Recv=%q, want nothing", i, string(b))
default:
}
}
msg1 := []byte("hello 0->1\n")
if err := clients[0].Send(clientKeys[1], msg1); err != nil {
t.Fatal(err)
}
recv(1, string(msg1))
recvNothing(0)
recvNothing(2)
msg2 := []byte("hello 1->2\n")
if err := clients[1].Send(clientKeys[2], msg2); err != nil {
t.Fatal(err)
}
recv(2, string(msg2))
recvNothing(0)
recvNothing(1)
}

@ -0,0 +1,13 @@
// 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 derp implements DERP, the Detour Encrypted Routing Protocol.
//
// DERP routes packets to clients using curve25519 keys as addresses.
//
// DERP is used by Tailscale nodes to proxy encrypted WireGuard
// packets through the Tailscale cloud servers when a direct path
// cannot be found or opened. DERP is a last resort. Both sides
// between very aggressive NATs, firewalls, no IPv6, etc? Well, DERP.
package derp

@ -0,0 +1,19 @@
module tailscale.com
go 1.13
require (
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
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/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/wireguard-go v0.0.0-20200208214841-2981baf46731
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5
gortc.io/stun v1.22.1
)

@ -0,0 +1,76 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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/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=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
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/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA=
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
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/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=
github.com/tailscale/wireguard-go v0.0.0-20200208161837-3cd0a483944a h1:vIyObUBvnXB1XTKTBM4AgoUFR9RHiz/kslGHClkXQVg=
github.com/tailscale/wireguard-go v0.0.0-20200208161837-3cd0a483944a/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
github.com/tailscale/wireguard-go v0.0.0-20200208214841-2981baf46731 h1:sNmny/5pHqHdm081Fx8rcNFnwt0zTGuee/0+Jz+tXCA=
github.com/tailscale/wireguard-go v0.0.0-20200208214841-2981baf46731/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a h1:aczoJ0HPNE92XKa7DrIzkNN6esOKO2TBwiiYoKcINhA=
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 h1:KOcEaR10tFr7gdJV2GCKw8Os5yED1u1aOqHjOAb6d2Y=
golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/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=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gortc.io/stun v1.22.1 h1:96mOdDATYRqhYB+TZdenWBg4CzL2Ye5kPyBXQ8KAB+8=
gortc.io/stun v1.22.1/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg=

@ -0,0 +1,79 @@
// 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 ipn
import (
"tailscale.com/control/controlclient"
"tailscale.com/tailcfg"
"tailscale.com/wgengine"
"time"
)
type State int
const (
NoState = State(iota)
NeedsLogin
NeedsMachineAuth
Stopped
Starting
Running
)
func (s State) String() string {
return [...]string{"NoState", "NeedsLogin", "NeedsMachineAuth",
"Stopped", "Starting", "Running"}[s]
}
type EngineStatus struct {
RBytes, WBytes wgengine.ByteCount
NumLive int
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
}
type NetworkMap = controlclient.NetworkMap
// In any given notification, any or all of these may be nil, meaning
// that they have not changed.
type Notify struct {
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any
LoginFinished *struct{} // event: login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
}
type Options struct {
FrontendLogID string // public logtail id used by frontend
ServerURL string
Prefs Prefs
LoginFlags controlclient.LoginFlags
Notify func(n Notify) `json:"-"`
}
type Backend interface {
// Start or restart the backend, because a new Handle has connected.
Start(opts Options) error
// Start a new interactive login. This should trigger a new
// BrowseToURL notification eventually.
StartLoginInteractive()
// Terminate the current login session and stop the wireguard engine.
Logout()
// Install a new set of user preferences, including WantRunning.
// This may cause the wireguard engine to reconfigure or stop.
SetPrefs(new Prefs)
// Poll for an update from the wireguard engine. Only needed if
// you want to display byte counts. Connection events are emitted
// automatically without polling.
RequestEngineStatus()
// Pretend the current key is going to expire after duration x.
// This is useful for testing GUIs to make sure they react properly
// with keys that are going to expire.
FakeExpireAfter(x time.Duration)
}

@ -0,0 +1,11 @@
// 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 ipn implements the interactions between the Tailscale cloud
// control plane and the local network stack.
//
// IPN is the abbreviated name for a Tailscale network. What's less
// clear is what it's an abbreviation for: Identified Private Network?
// IP Network? Internet Private Network? I Privately Network?
package ipn

@ -0,0 +1,207 @@
// 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 depends_on_currently_unreleased
package ipn
import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/tailscale/wireguard-go/tun/tuntest"
"tailscale.com/control/controlclient"
"tailscale.com/tailcfg"
"tailscale.com/testy"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
"tailscale.io/control" // not yet released
)
func TestIPN(t *testing.T) {
testy.FixLogs(t)
defer testy.UnfixLogs(t)
// Turn off STUN for the test to make it hermitic.
// TODO(crawshaw): add a test that runs against a local STUN server.
origDefaultSTUN := magicsock.DefaultSTUN
magicsock.DefaultSTUN = nil
defer func() {
magicsock.DefaultSTUN = origDefaultSTUN
}()
// TODO(apenwarr): Make resource checks actually pass.
// They don't right now, because (at least) wgengine doesn't fully
// shut down.
// rc := testy.NewResourceCheck()
// defer rc.Assert(t)
var ctl *control.Server
ctlHandler := func(w http.ResponseWriter, r *http.Request) {
ctl.ServeHTTP(w, r)
}
https := httptest.NewServer(http.HandlerFunc(ctlHandler))
serverURL := https.URL
defer https.Close()
defer https.CloseClientConnections()
tmpdir, err := ioutil.TempDir("", "ipntest")
if err != nil {
t.Fatalf("create tempdir: %v\n", err)
}
ctl, err = control.New(tmpdir, serverURL, true)
if err != nil {
t.Fatalf("create control server: %v\n", ctl)
}
n1 := newNode(t, "n1", https)
defer n1.Backend.Shutdown()
n1.Backend.StartLoginInteractive()
n2 := newNode(t, "n2", https)
defer n2.Backend.Shutdown()
n2.Backend.StartLoginInteractive()
var s1, s2 State
for {
t.Logf("\n\nn1.state=%v n2.state=%v\n\n", s1, s2)
// TODO(crawshaw): switch from || to &&. To do this we need to
// transmit some data so that the handshake completes on both
// sides. (Beacuse handshakes are 1RTT, it is the data
// transmission that completes the handshake.)
if s1 == Running || s2 == Running {
// TODO(apenwarr): ensure state sequence.
// Right now we'll just exit as soon as
// state==Running, even if the backend is lying or
// something. Not a great test.
break
}
select {
case n := <-n1.NotifyCh:
t.Logf("n1n: %v\n", n)
if n.State != nil {
s1 = *n.State
if s1 == NeedsMachineAuth {
authNode(t, ctl, n1.Backend)
}
}
case n := <-n2.NotifyCh:
t.Logf("n2n: %v\n", n)
if n.State != nil {
s2 = *n.State
if s2 == NeedsMachineAuth {
authNode(t, ctl, n2.Backend)
}
}
case <-time.After(3 * time.Second):
t.Fatalf("\n\n\nFATAL: timed out waiting for notifications.\n\n\n")
}
}
t.Skip("skipping ping tests, they are flaky") // TODO(crawshaw): this exposes a real bug!
n1addr := n1.Backend.NetMap().Addresses[0].IP
n2addr := n2.Backend.NetMap().Addresses[0].IP
t.Run("ping n2", func(t *testing.T) {
msg := tuntest.Ping(n2addr.IP(), n1addr.IP())
n1.ChannelTUN.Outbound <- msg
select {
case msgRecv := <-n2.ChannelTUN.Inbound:
if !bytes.Equal(msg, msgRecv) {
t.Error("bad ping")
}
case <-time.After(1 * time.Second):
t.Error("no ping seen")
}
})
t.Run("ping n1", func(t *testing.T) {
msg := tuntest.Ping(n1addr.IP(), n2addr.IP())
n2.ChannelTUN.Outbound <- msg
select {
case msgRecv := <-n1.ChannelTUN.Inbound:
if !bytes.Equal(msg, msgRecv) {
t.Error("bad ping")
}
case <-time.After(1 * time.Second):
t.Error("no ping seen")
}
})
}
type testNode struct {
Backend *LocalBackend
ChannelTUN *tuntest.ChannelTUN
NotifyCh <-chan Notify
}
// Create a new IPN node.
func newNode(t *testing.T, prefix string, https *httptest.Server) testNode {
t.Helper()
logfe := func(fmt string, args ...interface{}) {
t.Logf(prefix+".e: "+fmt, args...)
}
logf := func(fmt string, args ...interface{}) {
t.Logf(prefix+": "+fmt, args...)
}
derp := false
tun := tuntest.NewChannelTUN()
e1, err := wgengine.NewUserspaceEngineAdvanced(logfe, tun.TUN(), wgengine.NewFakeRouter, 0, derp)
if err != nil {
t.Fatalf("NewFakeEngine: %v\n", err)
}
n, err := NewLocalBackend(logf, prefix, e1)
if err != nil {
t.Fatalf("NewLocalBackend: %v\n", err)
}
nch := make(chan Notify, 1000)
c := controlclient.Persist{
Provider: "google",
LoginName: "test1@tailscale.com",
}
n.Start(Options{
FrontendLogID: prefix + "-f",
ServerURL: https.URL,
Prefs: Prefs{
RouteAll: true,
AllowSingleHosts: true,
CorpDNS: true,
WantRunning: true,
Persist: &c,
},
LoginFlags: controlclient.LoginDefault,
Notify: func(n Notify) {
// Automatically visit auth URLs
if n.BrowseToURL != nil {
t.Logf("\n\n\nURL! %vv\n", *n.BrowseToURL)
hc := https.Client()
_, err := hc.Get(*n.BrowseToURL)
if err != nil {
t.Logf("BrowseToURL: %v\n", err)
}
}
nch <- n
},
})
return testNode{
Backend: n,
ChannelTUN: tun,
NotifyCh: nch,
}
}
// Tell the control server to authorize the given node.
func authNode(t *testing.T, ctl *control.Server, n *LocalBackend) {
mk := *n.prefs.Persist.PrivateMachineKey.Public()
nk := *n.prefs.Persist.PrivateNodeKey.Public()
ctl.AuthorizeMachine(tailcfg.MachineKey(mk), tailcfg.NodeKey(nk))
}

@ -0,0 +1,72 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
import (
"log"
"time"
)
type FakeBackend struct {
serverURL string
notify func(n Notify)
live bool
}
func (b *FakeBackend) Start(opts Options) error {
b.serverURL = opts.ServerURL
if opts.Notify == nil {
log.Fatalf("FakeBackend.Start: opts.Notify is nil\n")
}
b.notify = opts.Notify
b.notify(Notify{Prefs: &opts.Prefs})
nl := NeedsLogin
b.notify(Notify{State: &nl})
return nil
}
func (b *FakeBackend) newState(s State) {
b.notify(Notify{State: &s})
if s == Running {
b.live = true
} else {
b.live = false
}
}
func (b *FakeBackend) StartLoginInteractive() {
u := b.serverURL + "/this/is/fake"
b.notify(Notify{BrowseToURL: &u})
b.newState(NeedsMachineAuth)
b.newState(Stopped)
// TODO(apenwarr): Fill in a more interesting netmap here.
b.notify(Notify{NetMap: &NetworkMap{}})
b.newState(Starting)
// TODO(apenwarr): Fill in a more interesting status.
b.notify(Notify{Engine: &EngineStatus{}})
b.newState(Running)
}
func (b *FakeBackend) Logout() {
b.newState(NeedsLogin)
}
func (b *FakeBackend) SetPrefs(new Prefs) {
b.notify(Notify{Prefs: &new})
if new.WantRunning && !b.live {
b.newState(Starting)
b.newState(Running)
} else if !new.WantRunning && b.live {
b.newState(Stopped)
}
}
func (b *FakeBackend) RequestEngineStatus() {
b.notify(Notify{Engine: &EngineStatus{}})
}
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &NetworkMap{}})
}

@ -0,0 +1,166 @@
// 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 ipn
import (
"strings"
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/logger"
)
type Handle struct {
serverURL string
frontendLogID string
b Backend
xnotify func(n Notify)
logf logger.Logf
// Mutex protects everything below
mu sync.Mutex
netmapCache *NetworkMap
engineStatusCache EngineStatus
stateCache State
prefsCache Prefs
}
func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
h := &Handle{
b: b,
logf: logf,
}
err := h.Start(opts)
if err != nil {
return nil, err
}
return h, nil
}
func (h *Handle) Start(opts Options) error {
h.serverURL = strings.TrimRight(opts.ServerURL, "/")
h.frontendLogID = opts.FrontendLogID
h.xnotify = opts.Notify
h.netmapCache = nil
h.engineStatusCache = EngineStatus{}
h.stateCache = NoState
h.prefsCache = opts.Prefs
xopts := opts
xopts.Notify = h.notify
return h.b.Start(xopts)
}
func (h *Handle) Reset() {
st := NoState
h.notify(Notify{State: &st})
}
func (h *Handle) notify(n Notify) {
h.mu.Lock()
if n.BackendLogID != nil {
h.logf("Handle: logs: be:%v fe:%v\n",
*n.BackendLogID, h.frontendLogID)
}
if n.State != nil {
h.stateCache = *n.State
}
if n.Prefs != nil {
h.prefsCache = *n.Prefs
}
if n.NetMap != nil {
h.netmapCache = n.NetMap
}
if n.Engine != nil {
h.engineStatusCache = *n.Engine
}
h.mu.Unlock()
if h.xnotify != nil {
// Forward onward to our parent's notifier
h.xnotify(n)
}
}
func (h *Handle) Prefs() Prefs {
h.mu.Lock()
defer h.mu.Unlock()
return h.prefsCache
}
func (h *Handle) UpdatePrefs(updateFn func(old Prefs) (new Prefs)) {
h.mu.Lock()
defer h.mu.Unlock()
new := updateFn(h.prefsCache)
h.prefsCache = new
h.b.SetPrefs(new)
}
func (h *Handle) State() State {
h.mu.Lock()
defer h.mu.Unlock()
return h.stateCache
}
func (h *Handle) EngineStatus() EngineStatus {
h.mu.Lock()
defer h.mu.Unlock()
return h.engineStatusCache
}
func (h *Handle) LocalAddrs() []wgcfg.CIDR {
h.mu.Lock()
defer h.mu.Unlock()
nm := h.netmapCache
if nm != nil {
return nm.Addresses
}
return []wgcfg.CIDR{}
}
func (h *Handle) NetMap() *NetworkMap {
h.mu.Lock()
defer h.mu.Unlock()
return h.netmapCache
}
func (h *Handle) Expiry() time.Time {
h.mu.Lock()
defer h.mu.Unlock()
nm := h.netmapCache
if nm != nil {
return nm.Expiry
}
return time.Time{}
}
func (h *Handle) AdminPageURL() string {
return h.serverURL + "/admin/machines"
}
func (h *Handle) StartLoginInteractive() {
h.b.StartLoginInteractive()
}
func (h *Handle) Logout() {
h.b.Logout()
}
func (h *Handle) RequestEngineStatus() {
h.b.RequestEngineStatus()
}
func (h *Handle) FakeExpireAfter(x time.Duration) {
h.b.FakeExpireAfter(x)
}

@ -0,0 +1,253 @@
// 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 ipnserver
import (
"bufio"
"context"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/google/go-cmp/cmp"
"github.com/klauspost/compress/zstd"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/logger"
"tailscale.com/logtail/backoff"
"tailscale.com/safesocket"
"tailscale.com/wgengine"
)
type Options struct {
SurviveDisconnects bool
AllowQuit bool
}
func pump(logf logger.Logf, ctx context.Context, bs *ipn.BackendServer, s net.Conn) {
defer logf("Control connection done.\n")
for ctx.Err() == nil && !bs.GotQuit {
msg, err := ipn.ReadMsg(s)
if err != nil {
logf("ReadMsg: %v\n", err)
break
}
err = bs.GotCommandMsg(msg)
if err != nil {
logf("GotCommandMsg: %v\n", err)
break
}
}
}
func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) error {
bo := backoff.Backoff{Name: "ipnserver"}
listen, _, err := safesocket.Listen("", "Tailscale", "tailscaled", 41112)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
b, err := ipn.NewLocalBackend(logf, logid, e)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
b.SetDecompressor(func() (controlclient.Decompressor, error) {
return zstd.NewReader(nil)
})
b.SetCmpDiff(func(x, y interface{}) string { return cmp.Diff(x, y) })
var s net.Conn
serverToClient := func(b []byte) {
if s != nil {
ipn.WriteMsg(s, b)
}
}
bs := ipn.NewBackendServer(logf, b, serverToClient)
logf("Listening on %v\n", listen.Addr())
// Go listeners can't take a context, close it instead.
go func() {
<-rctx.Done()
listen.Close()
}()
var oldS net.Conn
ctx, cancel := context.WithCancel(rctx)
stopAll := func() {
// Currently we only support one client connection at a time.
// Theoretically we could allow multiple clients, by passing
// notifications to all of them and accepting commands from
// any of them, but there doesn't seem to be much need for
// that right now.
if oldS != nil {
cancel()
safesocket.ConnCloseRead(oldS)
safesocket.ConnCloseWrite(oldS)
}
}
for i := 1; rctx.Err() == nil; i++ {
s, err = listen.Accept()
if err != nil {
logf("%d: Accept: %v\n", i, err)
bo.BackOff(rctx, err)
continue
}
logf("%d: Incoming control connection.\n", i)
stopAll()
ctx, cancel = context.WithCancel(context.Background())
oldS = s
go func(ctx context.Context, bs *ipn.BackendServer, s net.Conn, i int) {
si := fmt.Sprintf("%d: ", i)
pump(func(fmt string, args ...interface{}) {
logf(si+fmt, args...)
}, ctx, bs, s)
if !opts.SurviveDisconnects || bs.GotQuit {
bs.Reset()
s.Close()
}
if opts.AllowQuit {
os.Exit(0)
} else {
bs.GotQuit = false
}
}(ctx, bs, s, i)
bo.BackOff(ctx, nil)
}
stopAll()
return rctx.Err()
}
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
executable, err := os.Executable()
if err != nil {
panic("cannot determine executable: " + err.Error())
}
var proc struct {
mu sync.Mutex
p *os.Process
}
done := make(chan struct{})
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
var sig os.Signal
select {
case sig = <-interrupt:
logf("BabysitProc: got signal: %v\n", sig)
close(done)
case <-ctx.Done():
logf("BabysitProc: context done\n")
sig = os.Kill
close(done)
}
proc.mu.Lock()
proc.p.Signal(sig)
proc.mu.Unlock()
}()
bo := backoff.Backoff{Name: "BabysitProc"}
for {
startTime := time.Now()
log.Printf("exec: %#v %v\n", executable, args)
cmd := exec.Command(executable, args...)
// Create a pipe object to use as the subproc's stdin.
// When the writer goes away, the reader gets EOF.
// A subproc can watch its stdin and exit when it gets EOF;
// this is a very reliable way to have a subproc die when
// its parent (us) disappears.
// We never need to actually write to wStdin.
rStdin, wStdin, err := os.Pipe()
if err != nil {
log.Printf("os.Pipe 1: %v\n", err)
return
}
// Create a pipe object to use as the subproc's stdout/stderr.
// We'll read from this pipe and send it to logf, line by line.
// We can't use os.exec's io.Writer for this because it
// doesn't care about lines, and thus ends up merging multiple
// log lines into one or splitting one line into multiple
// logf() calls. bufio is more appropriate.
rStdout, wStdout, err := os.Pipe()
if err != nil {
log.Printf("os.Pipe 2: %v\n", err)
}
go func(r *os.File) {
defer r.Close()
rb := bufio.NewReader(r)
for {
s, err := rb.ReadString('\n')
if s != "" {
logf("%s\n", strings.TrimSuffix(s, "\n"))
}
if err != nil {
break
}
}
}(rStdout)
cmd.Stdin = rStdin
cmd.Stdout = wStdout
cmd.Stderr = wStdout
err = cmd.Start()
// Now that the subproc is started, get rid of our copy of the
// pipe reader. Bad things happen on Windows if more than one
// process owns the read side of a pipe.
rStdin.Close()
wStdout.Close()
if err != nil {
log.Printf("starting subprocess failed: %v", err)
} else {
proc.mu.Lock()
proc.p = cmd.Process
proc.mu.Unlock()
err = cmd.Wait()
log.Printf("subprocess exited: %v", err)
}
// If the process finishes, clean up the write side of the
// pipe. We'll make a new one when we restart the subproc.
wStdin.Close()
if time.Since(startTime) < 60*time.Second {
bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
} else {
// Reset the timeout, since the process ran for a while.
bo.BackOff(ctx, nil)
}
select {
case <-done:
return
default:
}
}
}

@ -0,0 +1,635 @@
// 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 ipn
import (
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/control/controlclient"
"tailscale.com/logger"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
// LocalBackend is the scaffolding between the Tailscale cloud control
// plane and the local network stack.
type LocalBackend struct {
logf logger.Logf
notify func(n Notify)
c *controlclient.Client
e wgengine.Engine
serverURL string
backendLogID string
portpoll *portlist.Poller // may be nil
newDecompressor func() (controlclient.Decompressor, error)
cmpDiff func(x, y interface{}) string
// The mutex protects the following elements.
mu sync.Mutex
prefs Prefs
state State
hiCache tailcfg.Hostinfo
netMapCache *controlclient.NetworkMap
engineStatus EngineStatus
endPoints []string
blocked bool
authURL string
interact int
// statusLock must be held before calling statusChanged.Lock() or
// statusChanged.Broadcast().
statusLock sync.Mutex
statusChanged *sync.Cond
}
func NewLocalBackend(logf logger.Logf, logid string, e wgengine.Engine) (*LocalBackend, error) {
if e == nil {
panic("ipn.NewLocalBackend: wgengine must not be nil")
}
// Default filter blocks everything, until Start() is called.
e.SetFilter(filter.NewAllowNone())
portpoll, err := portlist.NewPoller()
if err != nil {
logf("skipping portlist: %s\n", err)
}
b := LocalBackend{
logf: logf,
e: e,
backendLogID: logid,
state: NoState,
portpoll: portpoll,
}
b.statusChanged = sync.NewCond(&b.statusLock)
if b.portpoll != nil {
go b.portpoll.Run()
go b.runPoller()
}
return &b, nil
}
func (b *LocalBackend) Shutdown() {
if b.portpoll != nil {
b.portpoll.Close()
}
b.c.Shutdown()
b.e.Close()
b.e.Wait()
}
// SetDecompressor sets a decompression function, which must be a zstd
// reader.
//
// This exists because the iOS/Mac NetworkExtension is very resource
// constrained, and the zstd package is too heavy to fit in the
// constrained RSS limit.
func (b *LocalBackend) SetDecompressor(fn func() (controlclient.Decompressor, error)) {
b.newDecompressor = fn
}
// SetCmpDiff sets a comparison function used to generate logs of what
// has changed in the network map.
//
// Typically the comparison function comes from go-cmp.
// We don't wire it in directly here because the go-cmp package adds
// 1.77mb to the binary size of the iOS NetworkExtension, which takes
// away from its precious RSS limit.
func (b *LocalBackend) SetCmpDiff(cmpDiff func(x, y interface{}) string) {
b.cmpDiff = cmpDiff
}
func (b *LocalBackend) Start(opts Options) error {
if b.c != nil {
// TODO(apenwarr): avoid the need to reinit controlclient.
// This will trigger a full relogin/reconfigure cycle every
// time a Handle reconnects to the backend. Ideally, we
// would send the new Prefs and everything would get back
// into sync with the minimal changes. But that's not how it
// is right now, which is a sign that the code is still too
// complicated.
b.c.Shutdown()
}
b.logf("Start: %v\n", opts.Prefs.Pretty())
hi := controlclient.NewHostinfo()
hi.BackendLogID = b.backendLogID
hi.FrontendLogID = opts.FrontendLogID
b.mu.Lock()
hi.Services = b.hiCache.Services // keep any previous session
b.hiCache = hi
b.state = NoState
b.serverURL = opts.ServerURL
b.prefs = opts.Prefs
b.notify = opts.Notify
b.netMapCache = nil
b.mu.Unlock()
b.updateFilter()
var err error
persist := b.prefs.Persist
if persist == nil {
// let controlclient initialize it
persist = &controlclient.Persist{}
}
cli, err := controlclient.New(controlclient.Options{
Logf: func(fmt string, args ...interface{}) {
b.logf("control: "+fmt, args...)
},
Persist: *persist,
ServerURL: b.serverURL,
Hostinfo: &hi,
KeepAlive: true,
NewDecompressor: b.newDecompressor,
})
if err != nil {
return err
}
b.mu.Lock()
b.c = cli
b.mu.Unlock()
if b.endPoints != nil {
cli.UpdateEndpoints(0, b.endPoints)
}
cli.SetStatusFunc(func(new controlclient.Status) {
if new.LoginFinished != nil {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
b.authReconfig()
noargs := struct{}{}
b.send(Notify{LoginFinished: &noargs})
}
if new.Persist != nil {
persist := *new.Persist // copy
b.prefs.Persist = &persist
np := b.prefs
b.send(Notify{Prefs: &np})
}
if new.NetMap != nil {
if b.netMapCache != nil && b.cmpDiff != nil {
s1 := strings.Split(b.netMapCache.Concise(), "\n")
s2 := strings.Split(new.NetMap.Concise(), "\n")
b.logf("netmap diff:\n%v\n", b.cmpDiff(s1, s2))
}
b.netMapCache = new.NetMap
b.send(Notify{NetMap: new.NetMap})
b.updateFilter()
}
if new.URL != "" {
b.logf("Received auth URL: %.20v...\n", new.URL)
b.mu.Lock()
interact := b.interact
b.authURL = new.URL
b.mu.Unlock()
if interact > 0 {
b.popBrowserAuthNow()
}
}
if new.Err != "" {
// TODO(crawshaw): display in the UI.
log.Print(new.Err)
return
}
if new.NetMap != nil {
if b.prefs.WantRunning || b.State() == NeedsLogin {
b.prefs.WantRunning = true
}
b.SetPrefs(b.prefs)
}
b.stateMachine()
})
b.e.SetStatusCallback(func(s *wgengine.Status, err error) {
if err != nil {
b.logf("wgengine status error: %#v", err)
return
}
if s == nil {
log.Fatalf("weird: non-error wgengine update with status=nil\n")
}
b.mu.Lock()
es := b.parseWgStatus(s)
b.mu.Unlock()
b.engineStatus = es
if b.c != nil {
b.c.UpdateEndpoints(0, s.LocalAddrs)
}
b.endPoints = append([]string{}, s.LocalAddrs...)
b.stateMachine()
b.statusLock.Lock()
b.statusChanged.Broadcast()
b.statusLock.Unlock()
b.send(Notify{Engine: &es})
})
blid := b.backendLogID
b.logf("Backend: logs: be:%v fe:%v\n", blid, opts.FrontendLogID)
b.send(Notify{BackendLogID: &blid})
cli.Login(nil, opts.LoginFlags)
return nil
}
func (b *LocalBackend) updateFilter() {
if !b.Prefs().UsePacketFilter {
b.e.SetFilter(filter.NewAllowAll())
} else if b.netMapCache == nil {
// Not configured yet, block everything
b.e.SetFilter(filter.NewAllowNone())
} else {
b.logf("netmap packet filter: %v\n", b.netMapCache.PacketFilter)
b.e.SetFilter(filter.New(b.netMapCache.PacketFilter))
}
}
func (b *LocalBackend) runPoller() {
for {
ports := <-b.portpoll.C
if ports == nil {
break
}
sl := []tailcfg.Service{}
for _, p := range ports {
var proto tailcfg.ServiceProto
if p.Proto == "tcp" {
proto = tailcfg.TCP
} else if p.Proto == "udp" {
proto = tailcfg.UDP
}
if p.Port == 53 || p.Port == 68 ||
p.Port == 5353 || p.Port == 5355 {
// uninteresting system services
continue
}
s := tailcfg.Service{
Proto: proto,
Port: p.Port,
Description: p.Process,
}
sl = append(sl, s)
}
b.mu.Lock()
hi := b.hiCache
hi.Services = sl
b.hiCache = hi
cli := b.c
b.mu.Unlock()
// b.c might not be started yet
if cli != nil {
cli.SetHostinfo(hi)
}
}
}
func (b *LocalBackend) send(n Notify) {
if b.notify != nil {
n.Version = version.LONG
b.notify(n)
}
}
func (b *LocalBackend) popBrowserAuthNow() {
b.mu.Lock()
url := b.authURL
b.interact = 0
b.authURL = ""
b.mu.Unlock()
b.logf("popBrowserAuthNow: url=%v\n", url != "")
b.blockEngineUpdates(true)
b.stopEngineAndWait()
b.send(Notify{BrowseToURL: &url})
if b.State() == Running {
b.enterState(Starting)
}
}
func (b *LocalBackend) State() State {
b.mu.Lock()
defer b.mu.Unlock()
return b.state
}
func (b *LocalBackend) EngineStatus() EngineStatus {
b.mu.Lock()
defer b.mu.Unlock()
return b.engineStatus
}
func (b *LocalBackend) StartLoginInteractive() {
b.assertClient()
b.mu.Lock()
b.interact++
url := b.authURL
b.mu.Unlock()
b.logf("StartLoginInteractive: url=%v\n", url != "")
if url != "" {
b.popBrowserAuthNow()
} else {
b.c.Login(nil, controlclient.LoginInteractive)
}
}
func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
b.logf("FakeExpireAfter: %v\n", x)
if b.netMapCache != nil {
e := b.netMapCache.Expiry
if e.IsZero() || time.Until(e) > x {
b.netMapCache.Expiry = time.Now().Add(x)
}
b.send(Notify{NetMap: b.netMapCache})
}
}
func (b *LocalBackend) LocalAddrs() []wgcfg.CIDR {
if b.netMapCache != nil {
return b.netMapCache.Addresses
} else {
return nil
}
}
func (b *LocalBackend) Expiry() time.Time {
if b.netMapCache != nil {
return b.netMapCache.Expiry
} else {
return time.Time{}
}
}
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) EngineStatus {
var ss []string
var rx, tx wgengine.ByteCount
peers := make(map[tailcfg.NodeKey]wgengine.PeerStatus)
live := 0
for _, p := range s.Peers {
if p.LastHandshake.IsZero() {
ss = append(ss, "x")
} else {
ss = append(ss, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
live++
peers[p.NodeKey] = p
}
rx += p.RxBytes
tx += p.TxBytes
}
b.logf("v%v peers: %v\n", version.LONG, strings.Join(ss, " "))
return EngineStatus{
RBytes: rx,
WBytes: tx,
NumLive: live,
LivePeers: peers,
}
}
func (b *LocalBackend) AdminPageURL() string {
return b.serverURL + "/admin/machines"
}
func (b *LocalBackend) Prefs() Prefs {
b.mu.Lock()
defer b.mu.Unlock()
return b.prefs
}
func (b *LocalBackend) SetPrefs(new Prefs) {
b.mu.Lock()
old := b.prefs
new.Persist = old.Persist // caller isn't allowed to override this
b.prefs = new
b.mu.Unlock()
if old.WantRunning != new.WantRunning {
b.stateMachine()
} else {
b.authReconfig()
}
b.logf("SetPrefs: %v\n", new.Pretty())
b.send(Notify{Prefs: &new})
}
// Note: return value may be nil, if we haven't received a netmap yet.
func (b *LocalBackend) NetMap() *controlclient.NetworkMap {
return b.netMapCache
}
func (b *LocalBackend) blockEngineUpdates(block bool) {
// TODO(apenwarr): probably need mutex here (and several other places)
b.logf("blockEngineUpdates(%v)\n", block)
b.mu.Lock()
b.blocked = block
b.mu.Unlock()
}
func (b *LocalBackend) authReconfig() {
b.mu.Lock()
blocked := b.blocked
uc := b.prefs
nm := b.netMapCache
b.mu.Unlock()
if blocked {
b.logf("authReconfig: blocked, skipping.\n")
return
}
if nm == nil {
b.logf("authReconfig: netmap not yet valid. Skipping.\n")
return
}
if !uc.WantRunning {
b.logf("authReconfig: skipping because !WantRunning.\n")
return
}
b.logf("Configuring wireguard connection.\n")
uflags := controlclient.UDefault
if uc.RouteAll {
uflags |= controlclient.UAllowDefaultRoute
// TODO(apenwarr): Make subnet routes a different pref?
uflags |= controlclient.UAllowSubnetRoutes
// TODO(apenwarr): Remove this once we sort out subnet routes.
// Right now default routes are broken in Windows, but
// controlclient doesn't properly send subnet routes. So
// let's convert a default route into a subnet route in order
// to allow experimentation.
uflags |= controlclient.UHackDefaultRoute
}
if uc.AllowSingleHosts {
uflags |= controlclient.UAllowSingleHosts
}
b.logf("reconfig: ra=%v dns=%v 0x%02x\n", uc.RouteAll, uc.CorpDNS, uflags)
if nm != nil {
dns := nm.DNS
dom := nm.DNSDomains
if !uc.CorpDNS {
dns = []wgcfg.IP{}
dom = []string{}
}
cfg, err := nm.WGCfg(uflags, dns)
if err != nil {
log.Fatalf("WGCfg: %v\n", err)
}
err = b.e.Reconfig(cfg, dom)
if err != nil {
b.logf("reconfig: %v", err)
}
}
}
func (b *LocalBackend) enterState(newState State) {
b.mu.Lock()
state := b.state
prefs := b.prefs
b.mu.Unlock()
if state == newState {
return
}
b.logf("Switching ipn state %v -> %v (WantRunning=%v)\n",
state, newState, prefs.WantRunning)
if b.notify != nil {
b.send(Notify{State: &newState})
}
b.state = newState
switch newState {
case NeedsLogin:
b.blockEngineUpdates(true)
fallthrough
case Stopped:
err := b.e.Reconfig(&wgcfg.Config{}, nil)
if err != nil {
b.logf("Reconfig(down): %v\n", err)
}
case Starting, NeedsMachineAuth:
b.authReconfig()
// Needed so that UpdateEndpoints can run
b.e.RequestStatus()
case Running:
break
default:
b.logf("Weird: unknown newState %#v\n", newState)
}
}
func (b *LocalBackend) nextState() State {
b.assertClient()
state := b.State()
if b.netMapCache == nil {
if b.c.AuthCantContinue() {
// Auth was interrupted or waiting for URL visit,
// so it won't proceed without human help.
return NeedsLogin
} else {
// Auth or map request needs to finish
return state
}
} else if !b.prefs.WantRunning {
return Stopped
} else if e := b.netMapCache.Expiry; !e.IsZero() && time.Until(e) <= 0 {
return NeedsLogin
} else if b.netMapCache.MachineStatus != tailcfg.MachineAuthorized {
// TODO(crawshaw): handle tailcfg.MachineInvalid
return NeedsMachineAuth
} else if state == NeedsMachineAuth {
// (if we get here, we know MachineAuthorized == true)
return Starting
} else if state == Starting {
if b.EngineStatus().NumLive > 0 {
return Running
} else {
return state
}
} else if state == Running {
return Running
} else {
return Starting
}
}
func (b *LocalBackend) RequestEngineStatus() {
b.e.RequestStatus()
}
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
// Or maybe just call the state machine from fewer places.
func (b *LocalBackend) stateMachine() {
b.enterState(b.nextState())
}
func (b *LocalBackend) stopEngineAndWait() {
b.logf("stopEngineAndWait...\n")
b.e.Reconfig(&wgcfg.Config{}, nil)
b.requestEngineStatusAndWait()
b.logf("stopEngineAndWait: done.\n")
}
// Requests the wgengine status, and does not return until the status
// was delivered (to the usual callback).
func (b *LocalBackend) requestEngineStatusAndWait() {
b.logf("requestEngineStatusAndWait\n")
b.statusLock.Lock()
go b.e.RequestStatus()
b.logf("requestEngineStatusAndWait: waiting...\n")
b.statusChanged.Wait() // temporarily releases lock while waiting
b.logf("requestEngineStatusAndWait: got status update.\n")
b.statusLock.Unlock()
}
// NOTE(apenwarr): No easy way to persist logged-out status.
// Maybe that's for the better; if someone logs out accidentally,
// rebooting will fix it.
func (b *LocalBackend) Logout() {
b.assertClient()
b.netMapCache = nil
b.c.Logout()
b.netMapCache = nil
b.stateMachine()
}
func (b *LocalBackend) assertClient() {
if b.c == nil {
panic("LocalBackend.assertClient: b.c == nil")
}
}

@ -0,0 +1,249 @@
// 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 ipn
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"time"
"tailscale.com/logger"
"tailscale.com/version"
)
type NoArgs struct{}
type StartArgs struct {
Opts Options
}
type SetPrefsArgs struct {
New Prefs
}
type FakeExpireAfterArgs struct {
Duration time.Duration
}
// A command message sent to the server. Exactly one of these must be non-nil.
type Command struct {
Version string
Quit *NoArgs
Start *StartArgs
StartLoginInteractive *NoArgs
Logout *NoArgs
SetPrefs *SetPrefsArgs
RequestEngineStatus *NoArgs
FakeExpireAfter *FakeExpireAfterArgs
}
type BackendServer struct {
logf logger.Logf
b Backend // the Backend we are serving up
sendNotifyMsg func(b []byte) // send a notification message
GotQuit bool // a Quit command was received
}
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte)) *BackendServer {
return &BackendServer{
logf: logf,
b: b,
sendNotifyMsg: sendNotifyMsg,
}
}
func (bs *BackendServer) send(n Notify) {
n.Version = version.LONG
b, err := json.Marshal(n)
if err != nil {
log.Fatalf("Failed json.Marshal(notify): %v\n%#v\n", err, n)
}
bs.sendNotifyMsg(b)
}
// Inform the BackendServer of an incoming message.
func (bs *BackendServer) GotCommandMsg(b []byte) error {
cmd := Command{}
if err := json.Unmarshal(b, &cmd); err != nil {
return err
}
return bs.GotCommand(&cmd)
}
func (bs *BackendServer) GotCommand(cmd *Command) error {
if cmd.Version != version.LONG {
vs := fmt.Sprintf("Version mismatch! frontend=%#v backend=%#v\n",
cmd.Version, version.LONG)
bs.logf("%s\n", vs)
// ignore the command, but send a message back to the
// caller so it can realize the version mismatch too.
// We don't want to exit because it might cause a crash
// loop, and restarting won't fix the problem.
bs.send(Notify{
ErrMessage: &vs,
})
return nil
}
if cmd.Quit != nil {
bs.GotQuit = true
return errors.New("Quit command received")
}
if c := cmd.Start; c != nil {
opts := c.Opts
opts.Notify = bs.send
return bs.b.Start(opts)
} else if c := cmd.StartLoginInteractive; c != nil {
bs.b.StartLoginInteractive()
return nil
} else if c := cmd.Logout; c != nil {
bs.b.Logout()
return nil
} else if c := cmd.SetPrefs; c != nil {
bs.b.SetPrefs(c.New)
return nil
} else if c := cmd.RequestEngineStatus; c != nil {
bs.b.RequestEngineStatus()
return nil
} else if c := cmd.FakeExpireAfter; c != nil {
bs.b.FakeExpireAfter(c.Duration)
return nil
} else {
return fmt.Errorf("BackendServer.Do: no command specified")
}
}
func (bs *BackendServer) Reset() error {
// Tell the backend we got a Logout command, which will cause it
// to forget all its authentication information.
return bs.GotCommand(&Command{Logout: &NoArgs{}})
}
type BackendClient struct {
logf logger.Logf
sendCommandMsg func(b []byte)
notify func(n Notify)
}
func NewBackendClient(logf logger.Logf, sendCommandMsg func(b []byte)) *BackendClient {
return &BackendClient{
logf: logf,
sendCommandMsg: sendCommandMsg,
}
}
func (bc *BackendClient) GotNotifyMsg(b []byte) {
n := Notify{}
if err := json.Unmarshal(b, &n); err != nil {
log.Fatalf("BackendClient.Notify: cannot decode message")
}
if n.Version != version.LONG {
vs := fmt.Sprintf("Version mismatch! frontend=%#v backend=%#v",
version.LONG, n.Version)
bc.logf("%s\n", vs)
// delete anything in the notification except the version,
// to prevent incorrect operation.
n = Notify{
Version: n.Version,
ErrMessage: &vs,
}
}
if bc.notify != nil {
bc.notify(n)
}
}
func (bc *BackendClient) send(cmd Command) {
cmd.Version = version.LONG
b, err := json.Marshal(cmd)
if err != nil {
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
}
bc.sendCommandMsg(b)
}
func (bc *BackendClient) Quit() error {
bc.send(Command{Quit: &NoArgs{}})
return nil
}
func (bc *BackendClient) Start(opts Options) error {
bc.notify = opts.Notify
opts.Notify = nil // server can't call our function pointer
bc.send(Command{Start: &StartArgs{Opts: opts}})
return nil // remote Start() errors must be handled remotely
}
func (bc *BackendClient) StartLoginInteractive() {
bc.send(Command{StartLoginInteractive: &NoArgs{}})
}
func (bc *BackendClient) Logout() {
bc.send(Command{Logout: &NoArgs{}})
}
func (bc *BackendClient) SetPrefs(new Prefs) {
bc.send(Command{SetPrefs: &SetPrefsArgs{New: new}})
}
func (bc *BackendClient) RequestEngineStatus() {
bc.send(Command{RequestEngineStatus: &NoArgs{}})
}
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
}
const MSG_MAX = 1024 * 1024
// TODO(apenwarr): incremental json decode?
// That would let us avoid storing the whole byte array uselessly in RAM.
func ReadMsg(r io.Reader) ([]byte, error) {
cb := make([]byte, 4)
_, err := io.ReadFull(r, cb)
if err != nil {
return nil, err
}
n := binary.LittleEndian.Uint32(cb)
if n > 1024*1024 {
return nil, fmt.Errorf("ipn.Read: message too large: %v bytes", n)
}
b := make([]byte, n)
_, err = io.ReadFull(r, b)
if err != nil {
return nil, err
}
return b, nil
}
// TODO(apenwarr): incremental json encode?
// That would save RAM, at the expense of having to encode once so that
// we can produce the initial byte count.
func WriteMsg(w io.Writer, b []byte) error {
cb := make([]byte, 4)
if len(b) > MSG_MAX {
return fmt.Errorf("ipn.Write: message too large: %v bytes", len(b))
}
binary.LittleEndian.PutUint32(cb, uint32(len(b)))
n, err := w.Write(cb)
if err != nil {
return err
}
if n != 4 {
return fmt.Errorf("ipn.Write: short write: %v bytes (wanted 4)", n)
}
n, err = w.Write(b)
if err != nil {
return err
}
if n != len(b) {
return fmt.Errorf("ipn.Write: short write: %v bytes (wanted %v)", n, len(b))
}
return nil
}

@ -0,0 +1,171 @@
// 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 ipn
import (
"bytes"
"tailscale.com/testy"
"testing"
"time"
)
func TestReadWrite(t *testing.T) {
testy.FixLogs(t)
defer testy.UnfixLogs(t)
rc := testy.NewResourceCheck()
defer rc.Assert(t)
buf := bytes.Buffer{}
err := WriteMsg(&buf, []byte("Test string1"))
if err != nil {
t.Fatalf("write1: %v\n", err)
}
err = WriteMsg(&buf, []byte(""))
if err != nil {
t.Fatalf("write2: %v\n", err)
}
err = WriteMsg(&buf, []byte("Test3"))
if err != nil {
t.Fatalf("write3: %v\n", err)
}
b, err := ReadMsg(&buf)
if want, got := "Test string1", string(b); want != got {
t.Fatalf("read1: %#v != %#v\n", want, got)
}
b, err = ReadMsg(&buf)
if want, got := "", string(b); want != got {
t.Fatalf("read2: %#v != %#v\n", want, got)
}
b, err = ReadMsg(&buf)
if want, got := "Test3", string(b); want != got {
t.Fatalf("read3: %#v != %#v\n", want, got)
}
b, err = ReadMsg(&buf)
if err == nil {
t.Fatalf("read4: expected error, got %#v\n", b)
}
}
func TestClientServer(t *testing.T) {
testy.FixLogs(t)
defer testy.UnfixLogs(t)
rc := testy.NewResourceCheck()
defer rc.Assert(t)
b := &FakeBackend{}
var bs *BackendServer
var bc *BackendClient
serverToClientCh := make(chan []byte, 16)
defer close(serverToClientCh)
go func() {
for b := range serverToClientCh {
bc.GotNotifyMsg(b)
}
}()
serverToClient := func(b []byte) {
serverToClientCh <- append([]byte{}, b...)
}
clientToServer := func(b []byte) {
bs.GotCommandMsg(b)
}
slogf := func(fmt string, args ...interface{}) {
t.Logf("s: "+fmt, args...)
}
clogf := func(fmt string, args ...interface{}) {
t.Logf("c: "+fmt, args...)
}
bs = NewBackendServer(slogf, b, serverToClient)
bc = NewBackendClient(clogf, clientToServer)
ch := make(chan Notify, 256)
h, err := NewHandle(bc, clogf, Options{
ServerURL: "http://example.com/fake",
Notify: func(n Notify) {
ch <- n
},
})
if err != nil {
t.Fatalf("NewHandle error: %v\n", err)
}
notes := Notify{}
nn := []Notify{}
processNote := func(n Notify) {
nn = append(nn, n)
if n.State != nil {
t.Logf("state change: %v", *n.State)
notes.State = n.State
}
if n.Prefs != nil {
notes.Prefs = n.Prefs
}
if n.NetMap != nil {
notes.NetMap = n.NetMap
}
if n.Engine != nil {
notes.Engine = n.Engine
}
if n.BrowseToURL != nil {
notes.BrowseToURL = n.BrowseToURL
}
}
notesState := func() State {
if notes.State != nil {
return *notes.State
}
return NoState
}
flushUntil := func(wantFlush State) {
t.Helper()
timer := time.NewTimer(1 * time.Second)
loop:
for {
select {
case n := <-ch:
processNote(n)
if notesState() == wantFlush {
break loop
}
case <-timer.C:
t.Fatalf("timeout waiting for state %v, got %v", wantFlush, notes.State)
}
}
timer.Stop()
loop2:
for {
select {
case n := <-ch:
processNote(n)
default:
break loop2
}
}
if got, want := h.State(), notesState(); got != want {
t.Errorf("h.State()=%v, notes.State=%v (on flush until %v)\n", got, want, wantFlush)
}
}
flushUntil(NeedsLogin)
h.StartLoginInteractive()
flushUntil(Running)
if notes.NetMap == nil && h.NetMap() != nil {
t.Errorf("notes.NetMap == nil while h.NetMap != nil\nnotes:\n%v", nn)
}
h.UpdatePrefs(func(p Prefs) Prefs {
p.WantRunning = false
return p
})
flushUntil(Stopped)
h.Logout()
flushUntil(NeedsLogin)
}

@ -0,0 +1,149 @@
// 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 ipn
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
)
type Prefs struct {
RouteAll bool
AllowSingleHosts bool
CorpDNS bool
WantRunning bool
NotepadURLs bool
UsePacketFilter bool
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
// We can maybe do that once we're sure which module should persist
// it (backend or frontend?)
Persist *controlclient.Persist `json:"Config"`
}
func (uc *Prefs) Pretty() string {
var ucp string
if uc.Persist != nil {
ucp = uc.Persist.Pretty()
} else {
ucp = "Persist=nil"
}
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v %v}",
uc.RouteAll, uc.AllowSingleHosts, uc.CorpDNS, uc.WantRunning,
uc.NotepadURLs, ucp)
}
func (uc *Prefs) ToBytes() []byte {
data, err := json.MarshalIndent(uc, "", "\t")
if err != nil {
log.Fatalf("Prefs marshal: %v\n", err)
}
return data
}
func (uc *Prefs) Equals(uc2 *Prefs) bool {
b1 := uc.ToBytes()
b2 := uc2.ToBytes()
return bytes.Equal(b1, b2)
}
func NewPrefs() Prefs {
return Prefs{
// Provide default values for options which are normally
// true, but might be missing from the json data for any
// reason. The json can still override them to false.
RouteAll: true,
AllowSingleHosts: true,
CorpDNS: true,
WantRunning: true,
UsePacketFilter: true,
}
}
func PrefsFromBytes(b []byte, enforceDefaults bool) (Prefs, error) {
uc := NewPrefs()
if len(b) == 0 {
return uc, nil
}
persist := &controlclient.Persist{}
err := json.Unmarshal(b, persist)
if err == nil && (persist.Provider != "" || persist.LoginName != "") {
// old-style relaynode config; import it
uc.Persist = persist
} else {
err = json.Unmarshal(b, &uc)
if err != nil {
log.Printf("Prefs parse: %v: %v\n", err, b)
}
}
if enforceDefaults {
uc.RouteAll = true
uc.AllowSingleHosts = true
}
return uc, err
}
func (uc *Prefs) Copy() *Prefs {
uc2, err := PrefsFromBytes(uc.ToBytes(), false)
if err != nil {
log.Fatalf("Prefs was uncopyable: %v\n", err)
}
return &uc2
}
func LoadPrefs(filename string, enforceDefaults bool) Prefs {
log.Printf("Loading prefs %v\n", filename)
data, err := ioutil.ReadFile(filename)
uc := NewPrefs()
if err != nil {
log.Printf("Read: %v: %v\n", filename, err)
goto fail
}
uc, err = PrefsFromBytes(data, enforceDefaults)
if err != nil {
log.Printf("Parse: %v: %v\n", filename, err)
goto fail
}
goto post
fail:
log.Printf("failed to load config. Generating a new one.\n")
uc = NewPrefs()
uc.WantRunning = true
post:
// Update: we changed our minds :)
// Versabank would like to persist the setting across reboots, for now,
// because they don't fully trust the system and want to be able to
// leave it turned off when not in use. Eventually we need to make
// all motivation for this go away.
if false {
// Usability note: we always want WantRunning = true on startup.
// That way, if someone accidentally disables their VPN and doesn't
// know how, rebooting will fix it.
// We still persist WantRunning just in case we change our minds on
// this topic.
uc.WantRunning = true
}
log.Printf("Loaded prefs %v %v\n", filename, uc.Pretty())
return uc
}
func SavePrefs(filename string, uc *Prefs) {
log.Printf("Saving prefs %v %v\n", filename, uc.Pretty())
data := uc.ToBytes()
os.MkdirAll(filepath.Dir(filename), 0700)
if err := atomicfile.WriteFile(filename, data, 0666); err != nil {
log.Printf("SavePrefs: %v\n", err)
}
}

@ -0,0 +1,68 @@
// 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 ipn
import (
"testing"
"tailscale.com/control/controlclient"
)
func checkPrefs(t *testing.T, p Prefs) {
var err error
var p2, p2c Prefs
var p2b Prefs
pp := p.Pretty()
if pp == "" {
t.Fatalf("default p.Pretty() failed\n")
}
t.Logf("\npp: %#v\n", pp)
b := p.ToBytes()
if len(b) == 0 {
t.Fatalf("default p.ToBytes() failed\n")
}
if p != p {
t.Fatalf("p != p\n")
}
p2 = p
p2.RouteAll = true
if p == p2 {
t.Fatalf("p == p2\n")
}
p2b, err = PrefsFromBytes(p2.ToBytes(), false)
if err != nil {
t.Fatalf("PrefsFromBytes(p2) failed\n")
}
p2p := p2.Pretty()
p2bp := p2b.Pretty()
t.Logf("\np2p: %#v\np2bp: %#v\n", p2p, p2bp)
if p2p != p2bp {
t.Fatalf("p2p != p2bp\n%#v\n%#v\n", p2p, p2bp)
}
if !p2.Equals(&p2b) {
t.Fatalf("p2 != p2b\n%#v\n%#v\n", p2, p2b)
}
p2c = *p2.Copy()
if !p2b.Equals(&p2c) {
t.Fatalf("p2b != p2c\n")
}
}
func TestBasicPrefs(t *testing.T) {
p := Prefs{}
checkPrefs(t, p)
}
func TestPrefsPersist(t *testing.T) {
c := controlclient.Persist{
LoginName: "test@example.com",
}
p := Prefs{
CorpDNS: true,
Persist: &c,
}
checkPrefs(t, p)
}

@ -0,0 +1,10 @@
// 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 logger defines a type for writing to logs. It's just a
// convenience type so that we don't have to pass verbose func(...)
// types around.
package logger
type Logf func(fmt string, args ...interface{})

@ -0,0 +1,171 @@
// 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 logpolicy
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"github.com/klauspost/compress/zstd"
"golang.org/x/crypto/ssh/terminal"
"tailscale.com/atomicfile"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
"tailscale.com/version"
)
type Config struct {
Collection string
PrivateID logtail.PrivateID
PublicID logtail.PublicID
}
type Policy struct {
Logtail logtail.Logger
PublicID logtail.PublicID
}
func (c *Config) ToBytes() []byte {
data, err := json.MarshalIndent(c, "", "\t")
if err != nil {
log.Fatalf("logpolicy.Config marshal: %v\n", err)
}
return data
}
func (c *Config) Save(statefile string) {
c.PublicID = c.PrivateID.Public()
os.MkdirAll(filepath.Dir(statefile), 0777)
data := c.ToBytes()
if err := atomicfile.WriteFile(statefile, data, 0600); err != nil {
log.Printf("logpolicy.Config write: %v\n", err)
}
}
func ConfigFromBytes(b []byte) (*Config, error) {
c := &Config{}
if err := json.Unmarshal(b, c); err != nil {
return nil, err
}
return c, nil
}
type stderrWriter struct{}
// Always writes to the latest os.Stderr, even if os.Stderr changes
// during the lifetime of this object.
func (l *stderrWriter) Write(buf []byte) (int, error) {
return os.Stderr.Write(buf)
}
type logWriter struct {
logger *log.Logger
}
func (l *logWriter) Write(buf []byte) (int, error) {
l.logger.Print(string(buf))
return len(buf), nil
}
func New(collection string, filePrefix string) *Policy {
statefile := filePrefix + ".log.conf"
var lflags int
if terminal.IsTerminal(2) || runtime.GOOS == "windows" {
lflags = 0
} else {
lflags = log.LstdFlags
}
console := log.New(&stderrWriter{}, "", lflags)
var oldc *Config
data, err := ioutil.ReadFile(statefile)
if err != nil {
log.Printf("logpolicy.Read %v: %v\n", statefile, err)
oldc = &Config{}
oldc.Collection = collection
} else {
oldc, err = ConfigFromBytes(data)
if err != nil {
log.Printf("logpolicy.Config unmarshal: %v\n", err)
oldc = &Config{}
}
}
newc := *oldc
if newc.Collection != collection {
log.Printf("logpolicy.Config: config collection %q does not match %q", newc.Collection, collection)
// We picked up an incompatible config file.
// Regenerate the private ID.
newc.PrivateID = logtail.PrivateID{}
newc.Collection = collection
}
if newc.PrivateID == (logtail.PrivateID{}) {
newc.PrivateID, err = logtail.NewPrivateID()
if err != nil {
log.Fatalf("logpolicy: NewPrivateID() should never fail")
}
}
newc.PublicID = newc.PrivateID.Public()
if newc != *oldc {
newc.Save(statefile)
}
c := logtail.Config{
Collection: newc.Collection,
PrivateID: newc.PrivateID,
Stderr: &logWriter{console},
NewZstdEncoder: func() logtail.Encoder {
w, err := zstd.NewWriter(nil)
if err != nil {
panic(err)
}
return w
},
}
// TODO(crawshaw): filePrefix is a place meant to store configuration.
// OS policies usually have other preferred places to
// store logs. Use one of them?
filchBuf, filchErr := filch.New(filePrefix, filch.Options{})
if filchBuf != nil {
c.Buffer = filchBuf
}
lw := logtail.Log(c)
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(lw)
log.Printf("Program starting: v%v: %#v\n", version.LONG, os.Args)
log.Printf("LogID: %v\n", newc.PublicID)
if filchErr != nil {
log.Printf("filch failed: %v", err)
}
return &Policy{
Logtail: lw,
PublicID: newc.PublicID,
}
}
// Close immediately shuts down the logger.
func (p *Policy) Close() {
ctx, cancel := context.WithCancel(context.Background())
cancel()
p.Shutdown(ctx)
}
// Shutdown gracefully shuts down the logger, finishing any current
// log upload if it can be done before ctx is canceled.
func (p *Policy) Shutdown(ctx context.Context) error {
log.Printf("flushing log.\n")
if p.Logtail != nil {
return p.Logtail.Shutdown(ctx)
}
return nil
}

@ -0,0 +1,6 @@
*~
*.out
/example/logadopt/logadopt
/example/logreprocess/logreprocess
/example/logtail/logtail
/logtail

@ -0,0 +1,10 @@
# Tailscale Logs Service
This github repository contains libraries, documentation, and examples
for working with the public API of the tailscale logs service.
For a very quick introduction to the core features, read the
[API docs](api.md) and peruse the
[logs reprocessing](./example/logreprocess/demo.sh) example.
For more information, write to info@tailscale.io.

@ -0,0 +1,195 @@
# Tailscale Logs Service
The Tailscale Logs Service defines a REST interface for configuring, storing,
retrieving, and processing log entries.
# Overview
HTTP requests are received at the service **base URL**
[https://log.tailscale.io](https://log.tailscale.io), and return JSON-encoded
responses using standard HTTP response codes.
Authorization for the configuration and retrieval APIs is done with a secret
API key passed as the HTTP basic auth username. Secret keys are generated via
the web UI at base URL. An example of using basic auth with curl:
curl -u <log_api_key>: https://log.tailscale.io/collections
In the future, an HTTP header will allow using MessagePack instead of JSON.
## Collections
Logs are organized into collections. Inside each collection is any number of
instances.
A collection is a domain name. It is a grouping of related logs. As a
guideline, create one collection per product using subdomains of your
company's domain name. Collections must be registered with the logs service
before any attempt is made to store logs.
## Instances
Each collection is a set of instances. There is one instance per machine
writing logs.
An instance has a name and a number. An instance has a **private** and
**public** ID. The private ID is a 32-byte random number encoded as hex.
The public ID is the SHA-256 hash of the private ID, encoded as hex.
The private ID is used to write logs. The only copy of the private ID
should be on the machine sending logs. Ideally it is generated on the
machine. Logs can be written as soon as a private ID is generated.
The public ID is used to read and adopt logs. It is designed to be sent
to a service that also holds a logs service API key.
The tailscale logs service will store any logs for a short period of time.
To enable logs retention, the log can be **adopted** using the public ID
and a logs service API key.
Once this is done, logs will be retained long-term (for the configured
retention period).
Unadopted instance logs are stored temporarily to help with debugging:
a misconfigured machine writing logs with a bad ID can be spotted by
reading the logs.
If a public ID is not adopted, storage is tightly capped and logs are
deleted after 12 hours.
# APIs
## Storage
### `POST /c/<collection-name>/<private-ID>` — send a log
The body of the request is JSON.
A **single message** is an object with properties:
`{ }`
The client may send any properties it wants in the JSON message, except
for the `logtail` property which has special meaning. Inside the logtail
object the client may only set the following properties:
- `client_time` in the format of RFC3339: "2006-01-02T15:04:05.999999999Z07:00"
A future version of the logs service API will also support:
- `client_time_offset` a integer of nanoseconds since the client was reset
- `client_time_reset` a boolean if set to true resets the time offset counter
On receipt by the server the `client_time_offset` is transformed into a
`client_time` based on the `server_time` when the first (or
client_time_reset) event was received.
If any other properties are set in the logtail object they are moved into
the "error" field, the message is saved and a 4xx status code is returned.
A **batch of messages** is a JSON array filled with single message objects:
`[ { }, { }, ... ]`
If any of the array entries are not objects, the content is converted
into a message with a `"logtail": { "error": ...}` property, saved, and
a 4xx status code is returned.
Similarly any other request content not matching one of these formats is
saved in a logtail error field, and a 4xx status code is returned.
An invalid collection name returns `{"error": "invalid collection name"}`
along with a 403 status code.
Clients are encouraged to:
- POST as rapidly as possible (if not battery constrained). This minimizes
both the time necessary to see logs in a log viewer and the chance of
losing logs.
- Use HTTP/2 when streaming logs, as it does a much better job of
maintaining a TLS connection to minimize overhead for subsequent posts.
A future version of logs service API will support sending requests with
`Content-Encoding: zstd`.
## Retrieval
### `GET /collections` — query the set of collections and instances
Returns a JSON object listing all of the named collections.
The caller can query-encode the following fields:
- `collection-name` — limit the results to one collection
```
{
"collections": {
"collection1.yourcompany.com": {
"instances": {
"<logtail.PublicID>" :{
"first-seen": "timestamp",
"size": 4096
},
"<logtail.PublicID>" :{
"first-seen": "timestamp",
"size": 512000,
"orphan": true,
}
}
}
}
}
```
### `GET /c/<collection_name>` — query stored logs
The caller can query-encode the following fields:
- `instances` — zero or more log collection instances to limit results to
- `time-start` — the earliest log to include
- One of:
- `time-end` — the latest log to include
- `max-count` — maximum number of logs to return, allows paging
- `stream` — boolean that keeps the response dangling, streaming in
logs like `tail -f`. Incompatible with logtail-time-end.
In **stream=false** mode, the response is a single JSON object:
{
// TODO: header fields
"logs": [ {}, {}, ... ]
}
In **stream=true** mode, the response begins with a JSON header object
similar to the storage format, and then is a sequence of JSON log
objects, `{...}`, one per line. The server continues to send these until
the client closes the connection.
## Configuration
For organizations with a small number of instances writing logs, the
Configuration API are best used by a trusted human operator, usually
through a GUI. Organizations with many instances will need to automate
the creation of tokens.
### `POST /collections` — create or delete a collection
The caller must set the `collection` property and `action=create` or
`action=delete`, either form encoded or JSON encoded. Its character set
is restricted to the mundane: [a-zA-Z0-9-_.]+
Collection names are a global space. Typically they are a domain name.
### `POST /instances` — adopt an instance into a collection
The caller must send the following properties, form encoded or JSON encoded:
- `collection` — a valid FQDN ([a-zA-Z0-9-_.]+)
- `instances` an instance public ID encoded as hex
The collection name must be claimed by a group the caller belongs to.
The pair (collection-name, instance-public-ID) may or may not already have
logs associated with it.
On failure, an error message is returned with a 4xx or 5xx status code:
`{"error": "what went wrong"}`

@ -0,0 +1,49 @@
// 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 backoff
import (
"context"
"log"
"math/rand"
"time"
)
const MAX_BACKOFF_MSEC = 30000
type Backoff struct {
n int
Name string
NewTimer func(d time.Duration) *time.Timer
}
func (b *Backoff) BackOff(ctx context.Context, err error) {
if ctx.Err() == nil && err != nil {
b.n++
// n^2 backoff timer is a little smoother than the
// common choice of 2^n.
msec := b.n * b.n * 10
if msec > MAX_BACKOFF_MSEC {
msec = MAX_BACKOFF_MSEC
}
// Randomize the delay between 0.5-1.5 x msec, in order
// to prevent accidental "thundering herd" problems.
msec = rand.Intn(msec) + msec/2
log.Printf("%s: backoff: %d msec\n", b.Name, msec)
newTimer := b.NewTimer
if newTimer == nil {
newTimer = time.NewTimer
}
t := newTimer(time.Duration(msec) * time.Millisecond)
select {
case <-ctx.Done():
t.Stop()
case <-t.C:
}
} else {
// not a regular error
b.n = 0
}
}

@ -0,0 +1,82 @@
// 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 logtail
import (
"bytes"
"errors"
"fmt"
"sync"
)
type Buffer interface {
// TryReadLine tries to read a log line from the ring buffer.
// If no line is available it returns a nil slice.
// If the ring buffer is closed it returns io.EOF.
TryReadLine() ([]byte, error)
// Write writes a log line into the ring buffer.
Write([]byte) (int, error)
}
func NewMemoryBuffer(numEntries int) Buffer {
return &memBuffer{
pending: make(chan qentry, numEntries),
}
}
type memBuffer struct {
next []byte
pending chan qentry
dropMu sync.Mutex
dropCount int
}
func (m *memBuffer) TryReadLine() ([]byte, error) {
if m.next != nil {
msg := m.next
m.next = nil
return msg, nil
}
select {
case ent := <-m.pending:
if ent.dropCount > 0 {
m.next = ent.msg
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "----------- %d logs dropped ----------", ent.dropCount)
return buf.Bytes(), nil
}
return ent.msg, nil
default:
return nil, nil
}
}
func (m *memBuffer) Write(b []byte) (int, error) {
m.dropMu.Lock()
defer m.dropMu.Unlock()
ent := qentry{
msg: b,
dropCount: m.dropCount,
}
select {
case m.pending <- ent:
m.dropCount = 0
return len(b), nil
default:
m.dropCount++
return 0, errBufferFull
}
}
type qentry struct {
msg []byte
dropCount int
}
var errBufferFull = errors.New("logtail: buffer full")

@ -0,0 +1,51 @@
// 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 main
import (
"flag"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
)
func main() {
collection := flag.String("c", "", "logtail collection name")
publicID := flag.String("m", "", "machine public identifier")
apiKey := flag.String("p", "", "logtail API key")
flag.Parse()
if len(flag.Args()) != 0 {
flag.Usage()
os.Exit(1)
}
log.SetFlags(0)
req, err := http.NewRequest("POST", "https://log.tailscale.io/instances", strings.NewReader(url.Values{
"collection": []string{*collection},
"instances": []string{*publicID},
"adopt": []string{"true"},
}.Encode()))
if err != nil {
log.Fatal(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(*apiKey, "")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatalf("logadopt: response read failed %d: %v", resp.StatusCode, err)
}
if resp.StatusCode != 200 {
log.Fatalf("adoption failed: %d: %s", resp.StatusCode, string(b))
}
log.Printf("%s", string(b))
}

@ -0,0 +1,87 @@
#!/bin/bash
# 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.
#
# This shell script demonstrates writing logs from machines
# and then reprocessing those logs to amalgamate python tracebacks
# into a single log entry in a new collection.
#
# To run this demo, first install the example applications:
#
# go install tailscale.com/logtail/example/...
#
# Then generate a LOGTAIL_API_KEY and two test collections by visiting:
#
# https://log.tailscale.io
#
# Then set the three variables below.
trap 'rv=$?; [ "$rv" = 0 ] || echo "-- exiting with code $rv"; exit $rv' EXIT
set -e
LOG_TEXT='server starting
config file loaded
answering queries
Traceback (most recent call last):
File "/Users/crawshaw/junk.py", line 6, in <module>
main()
File "/Users/crawshaw/junk.py", line 4, in main
raise Exception("oops")
Exception: oops'
die() {
echo "$0: $*" >&2
exit 1
}
msg() {
echo "-- $*" >&2
}
if [ -z "$LOGTAIL_API_KEY" ]; then
die "LOGTAIL_API_KEY is not set"
fi
if [ -z "$COLLECTION_IN" ]; then
die "COLLECTION_IN is not set"
fi
if [ -z "$COLLECTION_OUT" ]; then
die "COLLECTION_OUT is not set"
fi
# Private IDs are 32-bytes of random hex.
# Normally you'd keep the same private IDs from one run to the next, but
# this is just an example.
msg "Generating keys..."
privateid1=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
privateid2=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
privateid3=$(hexdump -n 32 -e '8/4 "%08X"' /dev/urandom)
# Public IDs are the SHA-256 of the private ID.
publicid1=$(echo -n $privateid1 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
publicid2=$(echo -n $privateid2 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
publicid3=$(echo -n $privateid3 | xxd -r -p - | shasum -a 256 | sed 's/ -//')
# Write the machine logs to the input collection.
# Notice that this doesn't require an API key.
msg "Producing new logs..."
echo "$LOG_TEXT" | logtail -c $COLLECTION_IN -k $privateid1 >/dev/null
echo "$LOG_TEXT" | logtail -c $COLLECTION_IN -k $privateid2 >/dev/null
# Adopt the logs, so they will be kept and are readable.
msg "Adopting logs..."
logadopt -p "$LOGTAIL_API_KEY" -c "$COLLECTION_IN" -m $publicid1
logadopt -p "$LOGTAIL_API_KEY" -c "$COLLECTION_IN" -m $publicid2
# Reprocess the logs, amalgamating python tracebacks.
#
# We'll take that reprocessed output and write it to a separate collection,
# again via logtail.
#
# Time out quickly because all our "interesting" logs (generated
# above) have already been processed.
msg "Reprocessing logs..."
logreprocess -t 3s -c "$COLLECTION_IN" -p "$LOGTAIL_API_KEY" 2>&1 |
logtail -c "$COLLECTION_OUT" -k $privateid3

@ -0,0 +1,116 @@
// 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.
// The logreprocess program tails a log and reprocesses it.
package main
import (
"bufio"
"encoding/json"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"tailscale.com/logtail"
)
func main() {
collection := flag.String("c", "", "logtail collection name to read")
apiKey := flag.String("p", "", "logtail API key")
timeout := flag.Duration("t", 0, "timeout after which logreprocess quits")
flag.Parse()
if len(flag.Args()) != 0 {
flag.Usage()
os.Exit(1)
}
log.SetFlags(0)
if *timeout != 0 {
go func() {
<-time.After(*timeout)
log.Printf("logreprocess: timeout reached, quitting")
os.Exit(1)
}()
}
req, err := http.NewRequest("GET", "https://log.tailscale.io/c/"+*collection+"?stream=true", nil)
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(*apiKey, "")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("logreprocess: read error %d: %v", resp.StatusCode, err)
}
log.Fatalf("logreprocess: read error %d: %s", resp.StatusCode, string(b))
}
tracebackCache := make(map[logtail.PublicID]*ProcessedMsg)
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
var msg Msg
if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil {
log.Fatalf("logreprocess of %q: %v", string(scanner.Bytes()), err)
}
var pMsg *ProcessedMsg
if pMsg = tracebackCache[msg.Logtail.Instance]; pMsg != nil {
pMsg.Text += "\n" + msg.Text
if strings.HasPrefix(msg.Text, "Exception: ") {
delete(tracebackCache, msg.Logtail.Instance)
} else {
continue // write later
}
} else {
pMsg = &ProcessedMsg{
OrigInstance: msg.Logtail.Instance,
Text: msg.Text,
}
pMsg.Logtail.ClientTime = msg.Logtail.ClientTime
}
if strings.HasPrefix(msg.Text, "Traceback (most recent call last):") {
tracebackCache[msg.Logtail.Instance] = pMsg
continue // write later
}
b, err := json.Marshal(pMsg)
if err != nil {
log.Fatal(err)
}
log.Printf("%s", b)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
type Msg struct {
Logtail struct {
Instance logtail.PublicID `json:"instance"`
ClientTime time.Time `json:"client_time"`
} `json:"logtail"`
Text string `json:"text"`
}
type ProcessedMsg struct {
Logtail struct {
ClientTime time.Time `json:"client_time"`
} `json:"logtail"`
OrigInstance logtail.PublicID `json:"orig_instance"`
Text string `json:"text"`
}

@ -0,0 +1,46 @@
// 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.
// The logtail program logs stdin.
package main
import (
"bufio"
"flag"
"io"
"log"
"os"
"tailscale.com/logtail"
)
func main() {
collection := flag.String("c", "", "logtail collection name")
privateID := flag.String("k", "", "machine private identifier, 32-bytes in hex")
flag.Parse()
if len(flag.Args()) != 0 {
flag.Usage()
os.Exit(1)
}
log.SetFlags(0)
var id logtail.PrivateID
if err := id.UnmarshalText([]byte(*privateID)); err != nil {
log.Fatalf("logtail: bad -privateid: %v", err)
}
logger := logtail.Log(logtail.Config{
Collection: *collection,
PrivateID: id,
})
log.SetOutput(io.MultiWriter(logger, os.Stdout))
defer logger.Flush()
defer log.Printf("logtail exited")
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
log.Println(scanner.Text())
}
}

@ -0,0 +1,238 @@
// 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 filch is a file system queue that pilfers your stderr.
// (A FILe CHannel that filches.)
package filch
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"sync"
)
var stderrFD = 2 // a variable for testing
type Options struct {
ReplaceStderr bool // dup over fd 2 so everything written to stderr comes here
}
type Filch struct {
OrigStderr *os.File
mu sync.Mutex
cur *os.File
alt *os.File
altscan *bufio.Scanner
recovered int64
}
func (f *Filch) TryReadLine() ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.altscan != nil {
if b, err := f.scan(); b != nil || err != nil {
return b, err
}
}
f.cur, f.alt = f.alt, f.cur
if f.OrigStderr != nil {
if err := dup2Stderr(f.cur); err != nil {
return nil, err
}
}
if _, err := f.alt.Seek(0, os.SEEK_SET); err != nil {
return nil, err
}
f.altscan = bufio.NewScanner(f.alt)
f.altscan.Split(splitLines)
return f.scan()
}
func (f *Filch) scan() ([]byte, error) {
if f.altscan.Scan() {
return f.altscan.Bytes(), nil
}
err := f.altscan.Err()
err2 := f.alt.Truncate(0)
_, err3 := f.alt.Seek(0, os.SEEK_SET)
f.altscan = nil
if err != nil {
return nil, err
}
if err2 != nil {
return nil, err2
}
if err3 != nil {
return nil, err3
}
return nil, nil
}
func (f *Filch) Write(b []byte) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
if len(b) == 0 || b[len(b)-1] != '\n' {
bnl := make([]byte, len(b)+1)
copy(bnl, b)
bnl[len(bnl)-1] = '\n'
return f.cur.Write(bnl)
}
return f.cur.Write(b)
}
func (f *Filch) Close() (err error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.OrigStderr != nil {
if err2 := unsaveStderr(f.OrigStderr); err == nil {
err = err2
}
f.OrigStderr = nil
}
if err2 := f.cur.Close(); err == nil {
err = err2
}
if err2 := f.alt.Close(); err == nil {
err = err2
}
return err
}
func New(filePrefix string, opts Options) (f *Filch, err error) {
var f1, f2 *os.File
defer func() {
if err != nil {
if f1 != nil {
f1.Close()
}
if f2 != nil {
f2.Close()
}
err = fmt.Errorf("filch: %s", err)
}
}()
path1 := filePrefix + ".log1.txt"
path2 := filePrefix + ".log2.txt"
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return nil, err
}
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return nil, err
}
fi1, err := f1.Stat()
if err != nil {
return nil, err
}
fi2, err := f2.Stat()
if err != nil {
return nil, err
}
f = &Filch{
OrigStderr: os.Stderr, // temporary, for past logs recovery
}
// Neither, either, or both files may exist and contain logs from
// the last time the process ran. The three cases are:
//
// - neither: all logs were read out and files were truncated
// - either: logs were being written into one of the files
// - both: the files were swapped and were starting to be
// read out, while new logs streamed into the other
// file, but the read out did not complete
if n := fi1.Size() + fi2.Size(); n > 0 {
f.recovered = n
}
switch {
case fi1.Size() > 0 && fi2.Size() == 0:
f.cur, f.alt = f2, f1
case fi2.Size() > 0 && fi1.Size() == 0:
f.cur, f.alt = f1, f2
case fi1.Size() > 0 && fi2.Size() > 0: // both
// We need to pick one of the files to be the elder,
// which we do using the mtime.
var older, newer *os.File
if fi1.ModTime().Before(fi2.ModTime()) {
older, newer = f1, f2
} else {
older, newer = f2, f1
}
if err := moveContents(older, newer); err != nil {
fmt.Fprintf(f.OrigStderr, "filch: recover move failed: %v\n", err)
fmt.Fprintf(older, "filch: recover move failed: %v\n", err)
}
f.cur, f.alt = newer, older
default:
f.cur, f.alt = f1, f2 // does not matter
}
if f.recovered > 0 {
f.altscan = bufio.NewScanner(f.alt)
f.altscan.Split(splitLines)
}
f.OrigStderr = nil
if opts.ReplaceStderr {
f.OrigStderr, err = saveStderr()
if err != nil {
return nil, err
}
if err := dup2Stderr(f.cur); err != nil {
return nil, err
}
}
return f, nil
}
func moveContents(dst, src *os.File) (err error) {
defer func() {
_, err2 := src.Seek(0, os.SEEK_SET)
err3 := src.Truncate(0)
_, err4 := dst.Seek(0, os.SEEK_SET)
if err == nil {
err = err2
}
if err == nil {
err = err3
}
if err == nil {
err = err4
}
}()
if _, err := src.Seek(0, os.SEEK_SET); err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
return err
}
return nil
}
func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
return i + 1, data[0 : i+1], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}

@ -0,0 +1,178 @@
// 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 filch
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"unicode"
)
type filchTest struct {
*Filch
}
func newFilchTest(t *testing.T, filePrefix string, opts Options) *filchTest {
f, err := New(filePrefix, opts)
if err != nil {
t.Fatal(err)
}
return &filchTest{Filch: f}
}
func (f *filchTest) write(t *testing.T, s string) {
t.Helper()
if _, err := f.Write([]byte(s)); err != nil {
t.Fatal(err)
}
}
func (f *filchTest) read(t *testing.T, want string) {
t.Helper()
if b, err := f.TryReadLine(); err != nil {
t.Fatalf("r.ReadLine() err=%v", err)
} else if got := strings.TrimRightFunc(string(b), unicode.IsSpace); got != want {
t.Errorf("r.ReadLine()=%q, want %q", got, want)
}
}
func (f *filchTest) readEOF(t *testing.T) {
t.Helper()
if b, err := f.TryReadLine(); b != nil || err != nil {
t.Fatalf("r.ReadLine()=%q err=%v, want nil slice", string(b), err)
}
}
func (f *filchTest) close(t *testing.T) {
t.Helper()
if err := f.Close(); err != nil {
t.Fatal(err)
}
}
func genFilePrefix(t *testing.T) string {
t.Helper()
filePrefix, err := ioutil.TempDir("", "filch")
if err != nil {
t.Fatal(err)
}
return filepath.Join(filePrefix, "ringbuffer-")
}
func TestQueue(t *testing.T) {
filePrefix := genFilePrefix(t)
defer os.RemoveAll(filepath.Dir(filePrefix))
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.readEOF(t)
const line1 = "Hello, World!"
const line2 = "This is a test."
const line3 = "Of filch."
f.write(t, line1)
f.write(t, line2)
f.read(t, line1)
f.write(t, line3)
f.read(t, line2)
f.read(t, line3)
f.readEOF(t)
f.write(t, line1)
f.read(t, line1)
f.readEOF(t)
f.close(t)
}
func TestRecover(t *testing.T) {
t.Run("empty", func(t *testing.T) {
filePrefix := genFilePrefix(t)
defer os.RemoveAll(filepath.Dir(filePrefix))
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.read(t, "hello")
f.readEOF(t)
f.close(t)
f = newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.readEOF(t)
f.close(t)
})
t.Run("cur", func(t *testing.T) {
filePrefix := genFilePrefix(t)
defer os.RemoveAll(filepath.Dir(filePrefix))
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.close(t)
f = newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.read(t, "hello")
f.readEOF(t)
f.close(t)
})
t.Run("alt", func(t *testing.T) {
t.Skip("currently broken on linux, passes on macOS")
/* --- FAIL: TestRecover/alt (0.00s)
filch_test.go:128: r.ReadLine()="world", want "hello"
filch_test.go:129: r.ReadLine()="hello", want "world"
*/
filePrefix := genFilePrefix(t)
defer os.RemoveAll(filepath.Dir(filePrefix))
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.read(t, "hello")
f.write(t, "world")
f.close(t)
f = newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
// TODO(crawshaw): The "hello" log is replayed in recovery.
// We could reduce replays by risking some logs loss.
// What should our policy here be?
f.read(t, "hello")
f.read(t, "world")
f.readEOF(t)
f.close(t)
})
}
func TestFilchStderr(t *testing.T) {
pipeR, pipeW, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
defer pipeR.Close()
defer pipeW.Close()
stderrFD = int(pipeW.Fd())
defer func() {
stderrFD = 2
}()
filePrefix := genFilePrefix(t)
defer os.RemoveAll(filepath.Dir(filePrefix))
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: true})
f.write(t, "hello")
if _, err := fmt.Fprintf(pipeW, "filch\n"); err != nil {
t.Fatal(err)
}
f.read(t, "hello")
f.read(t, "filch")
f.readEOF(t)
f.close(t)
pipeW.Close()
b, err := ioutil.ReadAll(pipeR)
if err != nil {
t.Fatal(err)
}
if len(b) > 0 {
t.Errorf("unexpected write to fake stderr: %s", b)
}
}

@ -0,0 +1,30 @@
// 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
package filch
import (
"os"
"syscall"
)
func saveStderr() (*os.File, error) {
fd, err := syscall.Dup(stderrFD)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), "stderr"), nil
}
func unsaveStderr(f *os.File) error {
err := dup2Stderr(f)
f.Close()
return err
}
func dup2Stderr(f *os.File) error {
return syscall.Dup2(int(f.Fd()), stderrFD)
}

@ -0,0 +1,44 @@
// 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 filch
import (
"fmt"
"os"
"syscall"
)
var kernel32 = syscall.MustLoadDLL("kernel32.dll")
var procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
func setStdHandle(stdHandle int32, handle syscall.Handle) error {
r, _, e := syscall.Syscall(procSetStdHandle.Addr(), 2, uintptr(stdHandle), uintptr(handle), 0)
if r == 0 {
if e != 0 {
return error(e)
}
return syscall.EINVAL
}
return nil
}
func saveStderr() (*os.File, error) {
return os.Stderr, nil
}
func unsaveStderr(f *os.File) error {
os.Stderr = f
return nil
}
func dup2Stderr(f *os.File) error {
fd := int(f.Fd())
err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(fd))
if err != nil {
return fmt.Errorf("dup2Stderr: %w", err)
}
os.Stderr = f
return nil
}

@ -0,0 +1,103 @@
// 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 logtail
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
)
// PrivateID represents an instance that write logs.
// Private IDs are only shared with the server when writing logs.
type PrivateID [32]byte
// Safely generate a new PrivateId for use in Config objects.
// You should persist this across runs of an instance of your app, so that
// it can append to the same log file on each run.
func NewPrivateID() (id PrivateID, err error) {
_, err = rand.Read(id[:])
if err != nil {
return PrivateID{}, err
}
// Clamping, for future use.
id[0] &= 248
id[31] = (id[31] & 127) | 64
return id, nil
}
func (id PrivateID) MarshalText() ([]byte, error) {
b := make([]byte, hex.EncodedLen(len(id)))
if i := hex.Encode(b, id[:]); i != len(b) {
return nil, fmt.Errorf("logtail.PrivateID.MarhsalText: i=%d", i)
}
return b, nil
}
func (id *PrivateID) UnmarshalText(s []byte) error {
b, err := hex.DecodeString(string(s))
if err != nil {
return fmt.Errorf("logtail.PrivateID.UnmarshalText: %v", err)
}
if len(b) != len(id) {
return fmt.Errorf("logtail.PrivateID.UnmarshalText: invalid hex length: %d", len(b))
}
copy(id[:], b)
return nil
}
func (id PrivateID) String() string {
b, err := id.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}
func (id PrivateID) Public() (pub PublicID) {
var emptyID PrivateID
if id == emptyID {
panic("invalid logtail.Public() on an empty private ID")
}
h := sha256.New()
h.Write(id[:])
if n := copy(pub[:], h.Sum(pub[:0])); n != len(pub) {
panic(fmt.Sprintf("public id short copy: %d", n))
}
return pub
}
// PublicID represents an instance in the logs service for reading and adoption.
// The public ID value is a SHA-256 hash of a private ID.
type PublicID [sha256.Size]byte
func (id PublicID) MarshalText() ([]byte, error) {
b := make([]byte, hex.EncodedLen(len(id)))
if i := hex.Encode(b, id[:]); i != len(b) {
return nil, fmt.Errorf("logtail.PublicID.MarhsalText: i=%d", i)
}
return b, nil
}
func (id *PublicID) UnmarshalText(s []byte) error {
b, err := hex.DecodeString(string(s))
if err != nil {
return fmt.Errorf("logtail.PublicID.UnmarshalText: %v", err)
}
if len(b) != len(id) {
return fmt.Errorf("logtail.PublicID.UnmarshalText: invalid hex length: %d", len(b))
}
copy(id[:], b)
return nil
}
func (id PublicID) String() string {
b, err := id.MarshalText()
if err != nil {
panic(err)
}
return string(b)
}

@ -0,0 +1,54 @@
// 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 logtail
import (
"testing"
)
func TestIDs(t *testing.T) {
id1, err := NewPrivateID()
if err != nil {
t.Fatal(err)
}
pub1 := id1.Public()
id2, err := NewPrivateID()
if err != nil {
t.Fatal(err)
}
pub2 := id2.Public()
if id1 == id2 {
t.Fatalf("subsequent private IDs match: %v", id1)
}
if pub1 == pub2 {
t.Fatalf("subsequent public IDs match: %v", id1)
}
if id1.String() == id2.String() {
t.Fatalf("id1.String()=%v equals id2.String()", id1.String())
}
if pub1.String() == pub2.String() {
t.Fatalf("pub1.String()=%v equals pub2.String()", pub1.String())
}
id1txt, err := id1.MarshalText()
if err != nil {
t.Fatal(err)
}
var id3 PrivateID
if err := id3.UnmarshalText(id1txt); err != nil {
t.Fatal(err)
}
if id1 != id3 {
t.Fatalf("id1 %v: marshal and unmarshal gives different key: %v", id1, id3)
}
if want, got := id1.Public(), id3.Public(); want != got {
t.Fatalf("id1.Public()=%v does not match id3.Public()=%v", want, got)
}
if id1.String() != id3.String() {
t.Fatalf("id1.String()=%v does not match id3.String()=%v", id1.String(), id3.String())
}
}

@ -0,0 +1,464 @@
// 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 logtail sends logs to log.tailscale.io.
package logtail
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net/http"
"os"
"strconv"
"sync"
"time"
"tailscale.com/logtail/backoff"
)
type Logger interface {
// Write logs an encoded JSON blob.
//
// If the []byte passed to Write is not an encoded JSON blob,
// then contents is fit into a JSON blob and written.
//
// This is intended as an interface for the stdlib "log" package.
Write([]byte) (int, error)
// Flush uploads all logs to the server.
// It blocks until complete or there is an unrecoverable error.
Flush() error
// Shutdown gracefully shuts down the logger while completing any
// remaining uploads.
//
// It will block, continuing to try and upload unless the passed
// context object interrupts it by being done.
// If the shutdown is interrupted, an error is returned.
Shutdown(context.Context) error
// Close shuts down this logger object, the background log uploader
// process, and any associated goroutines.
//
// DEPRECATED: use Shutdown
Close()
}
type Encoder interface {
EncodeAll(src, dst []byte) []byte
Close() error
}
type Config struct {
Collection string // collection name, a domain name
PrivateID PrivateID // machine-specific private identifier
BaseURL string // if empty defaults to "https://log.tailscale.io"
HTTPC *http.Client // if empty defaults to http.DefaultClient
SkipClientTime bool // if true, client_time is not written to logs
LowMemory bool // if true, logtail minimizes memory use
TimeNow func() time.Time // if set, subsitutes uses of time.Now
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
Buffer Buffer // temp storage, if nil a MemoryBuffer
CheckLogs <-chan struct{} // signals Logger to check for filched logs to upload
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
}
func Log(cfg Config) Logger {
if cfg.BaseURL == "" {
cfg.BaseURL = "https://log.tailscale.io"
}
if cfg.HTTPC == nil {
cfg.HTTPC = http.DefaultClient
}
if cfg.TimeNow == nil {
cfg.TimeNow = time.Now
}
if cfg.Stderr == nil {
cfg.Stderr = os.Stderr
}
if cfg.Buffer == nil {
pendingSize := 256
if cfg.LowMemory {
pendingSize = 64
}
cfg.Buffer = NewMemoryBuffer(pendingSize)
}
if cfg.CheckLogs == nil {
cfg.CheckLogs = make(chan struct{})
}
l := &logger{
stderr: cfg.Stderr,
httpc: cfg.HTTPC,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
lowMem: cfg.LowMemory,
buffer: cfg.Buffer,
skipClientTime: cfg.SkipClientTime,
sent: make(chan struct{}, 1),
sentinel: make(chan int32, 16),
checkLogs: cfg.CheckLogs,
timeNow: cfg.TimeNow,
bo: backoff.Backoff{
Name: "logtail",
},
shutdownStart: make(chan struct{}),
shutdownDone: make(chan struct{}),
}
if cfg.NewZstdEncoder != nil {
l.zstdEncoder = cfg.NewZstdEncoder()
}
ctx, cancel := context.WithCancel(context.Background())
l.uploadCancel = cancel
go l.uploading(ctx)
l.Write([]byte("logtail started"))
return l
}
type logger struct {
stderr io.Writer
httpc *http.Client
url string
lowMem bool
skipClientTime bool
buffer Buffer
sent chan struct{} // signal to speed up drain
checkLogs <-chan struct{} // external signal to attempt a drain
sentinel chan int32
timeNow func() time.Time
bo backoff.Backoff
zstdEncoder Encoder
uploadCancel func()
shutdownStart chan struct{} // closed when shutdown begins
shutdownDone chan struct{} // closd when shutdown complete
dropMu sync.Mutex
dropCount int
}
func (l *logger) Shutdown(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
l.uploadCancel()
<-l.shutdownDone
case <-l.shutdownDone:
}
close(done)
}()
close(l.shutdownStart)
io.WriteString(l, "logger closing down\n")
<-done
if l.zstdEncoder != nil {
return l.zstdEncoder.Close()
}
return nil
}
func (l *logger) Close() {
l.Shutdown(nil)
}
func (l *logger) drainPending() (res []byte) {
buf := new(bytes.Buffer)
entries := 0
var batchDone bool
for buf.Len() < 1<<18 && !batchDone {
b, err := l.buffer.TryReadLine()
if err == io.EOF {
break
} else if err != nil {
b = []byte(fmt.Sprintf("reading ringbuffer: %v", err))
batchDone = true
} else if b == nil {
if entries > 0 {
break
}
select {
case <-l.shutdownStart:
batchDone = true
case <-l.checkLogs:
case <-l.sent:
}
continue
}
if len(b) == 0 {
continue
}
if b[0] != '{' || !json.Valid(b) {
// This is probably a log added to stderr by filch
// outside of the logtail logger. Encode it.
// Do not add a client time, as it could have been
// been written a long time ago.
b = l.encodeText(b, true)
}
switch {
case entries == 0:
buf.Write(b)
case entries == 1:
buf2 := new(bytes.Buffer)
buf2.WriteByte('[')
buf2.Write(buf.Bytes())
buf2.WriteByte(',')
buf2.Write(b)
buf.Reset()
buf.Write(buf2.Bytes())
default:
buf.WriteByte(',')
buf.Write(b)
}
entries++
}
if entries > 1 {
buf.WriteByte(']')
}
if buf.Len() == 0 {
return nil
}
return buf.Bytes()
}
var clientSentinelPrefix = []byte(`{"logtail":{"client_sentinel":`)
const (
noSentinel = 0
stopSentinel = 1
)
// newSentinel creates a client sentinel between 2 and maxint32.
// It does not generate the reserved values:
// 0 is no sentinel
// 1 is stop the logger
func newSentinel() ([]byte, int32) {
val, err := rand.Int(rand.Reader, big.NewInt(1<<31-2))
if err != nil {
panic(err)
}
v := int32(val.Int64()) + 2
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "%s%d}}\n", clientSentinelPrefix, v)
return buf.Bytes(), v
}
// readSentinel reads a sentinel.
// If it is not a sentinel it reports 0.
func readSentinel(b []byte) int32 {
if !bytes.HasPrefix(b, clientSentinelPrefix) {
return 0
}
b = bytes.TrimPrefix(b, clientSentinelPrefix)
b = bytes.TrimSuffix(bytes.TrimSpace(b), []byte("}}"))
v, err := strconv.Atoi(string(b))
if err != nil {
return 0
}
return int32(v)
}
// This is the goroutine that repeatedly uploads logs in the background.
func (l *logger) uploading(ctx context.Context) {
defer close(l.shutdownDone)
for {
body := l.drainPending()
if l.zstdEncoder != nil {
body = l.zstdEncoder.EncodeAll(body, nil)
}
for len(body) > 0 {
select {
case <-ctx.Done():
return
default:
}
uploaded, err := l.upload(ctx, body)
if err != nil {
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
}
if uploaded {
break
}
l.bo.BackOff(ctx, err)
}
select {
case <-l.shutdownStart:
return
default:
}
}
}
func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err error) {
req, err := http.NewRequest("POST", l.url, bytes.NewReader(body))
if err != nil {
// I know of no conditions under which this could fail.
// Report it very loudly.
// TODO record logs to disk
panic("logtail: cannot build http request: " + err.Error())
}
if l.zstdEncoder != nil {
req.Header.Add("Content-Encoding", "zstd")
}
maxUploadTime := 45 * time.Second
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
defer cancel()
req = req.WithContext(ctx)
compressedNote := "not-compressed"
if l.zstdEncoder != nil {
compressedNote = "compressed"
}
resp, err := l.httpc.Do(req)
if err != nil {
return false, fmt.Errorf("log upload of %d bytes %s failed: %v", len(body), compressedNote, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
uploaded = resp.StatusCode == 400 // the server saved the logs anyway
b, _ := ioutil.ReadAll(resp.Body)
return uploaded, fmt.Errorf("log upload of %d bytes %s failed %d: %q", len(body), compressedNote, resp.StatusCode, string(b))
}
return true, nil
}
func (l *logger) Flush() error {
return nil
}
var errHasLogtail = errors.New("logtail: JSON log message contains reserved 'logtail' property")
func (l *logger) send(jsonBlob []byte) (int, error) {
n, err := l.buffer.Write(jsonBlob)
select {
case l.sent <- struct{}{}:
default:
}
return n, err
}
func (l *logger) encodeText(buf []byte, skipClientTime bool) []byte {
now := l.timeNow()
b := make([]byte, 0, len(buf)+16)
b = append(b, '{')
if !skipClientTime {
b = append(b, `"logtail": {"client_time": "`...)
b = now.AppendFormat(b, time.RFC3339Nano)
b = append(b, "\"}, "...)
}
b = append(b, "\"text\": \""...)
for i, c := range buf {
switch c {
case '\b':
b = append(b, '\\', 'b')
case '\f':
b = append(b, '\\', 'f')
case '\n':
b = append(b, '\\', 'n')
case '\r':
b = append(b, '\\', 'r')
case '\t':
b = append(b, '\\', 't')
case '"':
b = append(b, '\\', '"')
case '\\':
b = append(b, '\\', '\\')
default:
b = append(b, c)
}
if l.lowMem && i > 254 {
b = append(b, "…"...)
break
}
}
b = append(b, "\"}\n"...)
return b
}
func (l *logger) encode(buf []byte) []byte {
if buf[0] != '{' {
return l.encodeText(buf, l.skipClientTime) // text fast-path
}
now := l.timeNow()
obj := make(map[string]interface{})
if err := json.Unmarshal(buf, &obj); err != nil {
for k := range obj {
delete(obj, k)
}
obj["text"] = string(buf)
}
if txt, isStr := obj["text"].(string); l.lowMem && isStr && len(txt) > 254 {
// TODO(crawshaw): trim to unicode code point
obj["text"] = txt[:254] + "…"
}
hasLogtail := obj["logtail"] != nil
if hasLogtail {
obj["error_has_logtail"] = obj["logtail"]
obj["logtail"] = nil
}
if !l.skipClientTime {
obj["logtail"] = map[string]string{
"client_time": now.Format(time.RFC3339Nano),
}
}
b, err := json.Marshal(obj)
if err != nil {
fmt.Fprintf(l.stderr, "logtail: re-encoding JSON failed: %v\n", err)
// I know of no conditions under which this could fail.
// Report it very loudly.
panic("logtail: re-encoding JSON failed: " + err.Error())
}
b = append(b, '\n')
return b
}
func (l *logger) Write(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, nil
}
if l.stderr != nil && l.stderr != ioutil.Discard {
if buf[len(buf)-1] == '\n' {
l.stderr.Write(buf)
} else {
// The log package always line-terminates logs,
// so this is an uncommon path.
bufnl := make([]byte, len(buf)+1)
copy(bufnl, buf)
bufnl[len(bufnl)-1] = '\n'
l.stderr.Write(bufnl)
}
}
b := l.encode(buf)
return l.send(b)
}

@ -0,0 +1,20 @@
// 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 logtail
import (
"context"
"testing"
)
func TestFastShutdown(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
l := Log(Config{
BaseURL: "http://localhost:1234",
})
l.Shutdown(ctx)
}

@ -0,0 +1,155 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"fmt"
"sort"
"strconv"
"strings"
exec "tailscale.com/tempfork/osexec"
)
func parsePort(s string) int {
// a.b.c.d:1234 or [a:b:c:d]:1234
i1 := strings.LastIndexByte(s, ':')
// a.b.c.d.1234 or [a:b:c:d].1234
i2 := strings.LastIndexByte(s, '.')
i := i1
if i2 > i {
i = i2
}
if i < 0 {
// no match; weird
return -1
}
portstr := s[i+1 : len(s)]
if portstr == "*" {
return 0
}
port, err := strconv.ParseUint(portstr, 10, 16)
if err != nil {
// invalid port; weird
return -1
}
return int(port)
}
type nothing struct{}
// Lowest common denominator parser for "netstat -na" format.
// All of Linux, Windows, and macOS support -na and give similar-ish output
// formats that we can parse without special detection logic.
// Unfortunately, options to filter by proto or state are non-portable,
// so we'll filter for ourselves.
func parsePortsNetstat(output string) List {
m := map[Port]nothing{}
lines := strings.Split(string(output), "\n")
var lastline string
var lastport Port
for _, line := range lines {
trimline := strings.TrimSpace(line)
cols := strings.Fields(trimline)
if len(cols) < 1 {
continue
}
protos := strings.ToLower(cols[0])
var proto, laddr, raddr string
if strings.HasPrefix(protos, "tcp") {
if len(cols) < 4 {
continue
}
proto = "tcp"
laddr = cols[len(cols)-3]
raddr = cols[len(cols)-2]
state := cols[len(cols)-1]
if !strings.HasPrefix(state, "LISTEN") {
// not interested in non-listener sockets
continue
}
} else if strings.HasPrefix(protos, "udp") {
if len(cols) < 3 {
continue
}
proto = "udp"
laddr = cols[len(cols)-2]
raddr = cols[len(cols)-1]
} else if protos[0] == '[' && len(trimline) > 2 {
// Windows: with netstat -nab, appends a line like:
// [description]
// after the port line.
p := lastport
delete(m, lastport)
proc := trimline[1 : len(trimline)-1]
if proc == "svchost.exe" && lastline != "" {
p.Process = lastline
} else {
if strings.HasSuffix(proc, ".exe") {
p.Process = proc[:len(proc)-4]
} else {
p.Process = proc
}
}
m[p] = nothing{}
} else {
// not interested in other protocols
lastline = trimline
continue
}
lport := parsePort(laddr)
rport := parsePort(raddr)
if rport != 0 || lport <= 0 {
// not interested in "connected" sockets
continue
}
p := Port{
Proto: proto,
Port: uint16(lport),
}
m[p] = nothing{}
lastport = p
lastline = ""
}
l := []Port{}
for p := range m {
l = append(l, p)
}
sort.Slice(l, func(i, j int) bool {
return (&l[i]).lessThan(&l[j])
})
return l
}
func listPortsNetstat(args string) (List, error) {
exe, err := exec.LookPath("netstat")
if err != nil {
return nil, fmt.Errorf("netstat: lookup: %v", err)
}
c := exec.Cmd{
Path: exe,
Args: []string{exe, args},
}
output, err := c.Output()
if err != nil {
xe, ok := err.(*exec.ExitError)
stderr := ""
if ok {
stderr = strings.TrimSpace(string(xe.Stderr))
}
return nil, fmt.Errorf("netstat: %v (%q)", err, stderr)
}
return parsePortsNetstat(string(output)), nil
}

@ -0,0 +1,89 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"fmt"
"testing"
)
func TestParsePort(t *testing.T) {
type InOut struct {
in string
expect int
}
tests := []InOut{
InOut{"1.2.3.4:5678", 5678},
InOut{"0.0.0.0.999", 999},
InOut{"1.2.3.4:*", 0},
InOut{"5.5.5.5:0", 0},
InOut{"[1::2]:5", 5},
InOut{"[1::2].5", 5},
InOut{"gibberish", -1},
}
for _, io := range tests {
got := parsePort(io.in)
if got != io.expect {
t.Fatalf("input:%#v expect:%v got:%v\n", io.in, io.expect, got)
}
}
}
var netstat_output = `
// linux
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
udp 0 0 0.0.0.0:5353 0.0.0.0:*
udp6 0 0 :::5353 :::*
udp6 0 0 :::5354 :::*
// macOS
tcp4 0 0 *.23 *.* LISTEN
tcp6 0 0 *.24 *.* LISTEN
udp6 0 0 *.5453 *.*
udp4 0 0 *.5553 *.*
// Windows 10
Proto Local Address Foreign Address State
TCP 0.0.0.0:32 0.0.0.0:0 LISTENING
[sshd.exe]
UDP 0.0.0.0:5050 *:*
CDPSvc
[svchost.exe]
UDP 0.0.0.0:53 *:*
[chrome.exe]
UDP 10.0.1.43:9353 *:*
[iTunes.exe]
UDP [::]:53 *:*
UDP [::]:53 *:*
[funball.exe]
`
func TestParsePortsNetstat(t *testing.T) {
expect := List{
Port{"tcp", 22, "", ""},
Port{"tcp", 23, "", ""},
Port{"tcp", 24, "", ""},
Port{"tcp", 32, "", "sshd"},
Port{"udp", 53, "", "chrome"},
Port{"udp", 53, "", "funball"},
Port{"udp", 5050, "", "CDPSvc"},
Port{"udp", 5353, "", ""},
Port{"udp", 5354, "", ""},
Port{"udp", 5453, "", ""},
Port{"udp", 5553, "", ""},
Port{"udp", 9353, "", "iTunes"},
}
pl := parsePortsNetstat(netstat_output)
fmt.Printf("--- expect:\n%v\n", expect)
fmt.Printf("--- got:\n%v\n", pl)
for i := range pl {
if expect[i] != pl[i] {
t.Fatalf("row#%d\n expect=%v\n got=%v\n",
i, expect[i], pl[i])
}
}
}

@ -0,0 +1,59 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"time"
)
type Poller struct {
C chan List // new data when it arrives; closed when done
quitCh chan struct{} // close this to force exit
Err error // last returned error code, if any
prev List // most recent data
}
func NewPoller() (*Poller, error) {
p := &Poller{
C: make(chan List),
quitCh: make(chan struct{}),
}
// Do one initial poll synchronously, so the caller can react
// to any obvious errors.
p.prev, p.Err = GetList(nil)
return p, p.Err
}
func (p *Poller) Close() {
close(p.quitCh)
<-p.C
}
// Poll periodically. Run this in a goroutine if you want.
func (p *Poller) Run() error {
defer close(p.C)
tick := time.NewTicker(POLL_SECONDS * time.Second)
defer tick.Stop()
// Send out the pre-generated initial value
p.C <- p.prev
for {
select {
case <-tick.C:
pl, err := GetList(p.prev)
if err != nil {
p.Err = err
return p.Err
}
if !pl.SameInodes(p.prev) {
p.prev = pl
p.C <- pl
}
case <-p.quitCh:
return nil
}
}
}

@ -0,0 +1,87 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"fmt"
"strings"
)
type Port struct {
Proto string
Port uint16
inode string
Process string
}
type List []Port
var protos = []string{"tcp", "udp"}
func (a *Port) lessThan(b *Port) bool {
if a.Port < b.Port {
return true
} else if a.Port > b.Port {
return false
}
if a.Proto < b.Proto {
return true
} else if a.Proto > b.Proto {
return false
}
if a.inode < b.inode {
return true
} else if a.inode > b.inode {
return false
}
if a.Process < b.Process {
return true
} else if a.Process > b.Process {
return false
}
return false
}
func (a List) SameInodes(b List) bool {
if a == nil || b == nil || len(a) != len(b) {
return false
}
for i := range a {
if a[i].Proto != b[i].Proto ||
a[i].Port != b[i].Port ||
a[i].inode != b[i].inode {
return false
}
}
return true
}
func (pl List) String() string {
out := []string{}
for _, v := range pl {
out = append(out, fmt.Sprintf("%-3s %5d %-17s %#v",
v.Proto, v.Port, v.inode, v.Process))
}
return strings.Join(out, "\n")
}
func GetList(prev List) (List, error) {
pl, err := listPorts()
if err != nil {
return nil, fmt.Errorf("listPorts: %s", err)
}
if pl.SameInodes(prev) {
// Nothing changed, skip inode lookup
return prev, nil
}
pl, err = addProcesses(pl)
if err != nil {
return nil, fmt.Errorf("addProcesses: %s", err)
}
return pl, nil
}

@ -0,0 +1,99 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows
package portlist
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"strings"
exec "tailscale.com/tempfork/osexec"
)
// We have to run netstat, which is a bit expensive, so don't do it too often.
const POLL_SECONDS = 5
func listPorts() (List, error) {
return listPortsNetstat("-na")
}
// In theory, lsof could replace the function of both listPorts() and
// addProcesses(), since it provides a superset of the netstat output.
// However, "netstat -na" runs ~100x faster than lsof on my machine, so
// we should do it only if the list of open ports has actually changed.
//
// TODO(apenwarr): this fails in a macOS sandbox (ie. our usual case).
// We might as well just delete this code if we can't find a solution.
func addProcesses(pl []Port) ([]Port, error) {
exe, err := exec.LookPath("lsof")
if err != nil {
return nil, fmt.Errorf("lsof: lookup: %v", err)
}
c := exec.Cmd{
Path: exe,
Args: []string{exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6"},
}
output, err := c.Output()
if err != nil {
xe, ok := err.(*exec.ExitError)
stderr := ""
if ok {
stderr = strings.TrimSpace(string(xe.Stderr))
}
// fails when run in a macOS sandbox, so make this non-fatal.
log.Printf("portlist: lsof: %v (%q)\n", err, stderr)
return pl, nil
}
type ProtoPort struct {
proto string
port uint16
}
m := map[ProtoPort]*Port{}
for i := range pl {
pp := ProtoPort{pl[i].Proto, pl[i].Port}
m[pp] = &pl[i]
}
r := bytes.NewReader(output)
scanner := bufio.NewScanner(r)
var cmd, proto string
for scanner.Scan() {
line := scanner.Text()
if line[0] == 'p' {
// starting a new process
cmd = ""
proto = ""
} else if line[0] == 'c' {
cmd = line[1:len(line)]
} else if line[0] == 'P' {
proto = strings.ToLower(line[1:len(line)])
} else if line[0] == 'n' {
rest := line[1:len(line)]
i := strings.Index(rest, "->")
if i < 0 {
// a listening port
port := parsePort(rest)
if port > 0 {
pp := ProtoPort{proto, uint16(port)}
p := m[pp]
if p != nil {
p.Process = cmd
} else {
fmt.Fprintf(os.Stderr, "weird: missing %v\n", pp)
}
}
}
}
}
return pl, nil
}

@ -0,0 +1,155 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strconv"
"strings"
)
// Reading the sockfiles on Linux is very fast, so we can do it often.
const POLL_SECONDS = 1
// TODO(apenwarr): Include IPv6 ports eventually.
// Right now we don't route IPv6 anyway so it's better to exclude them.
var sockfiles = []string{"/proc/net/tcp", "/proc/net/udp"}
func listPorts() (List, error) {
l := []Port{}
for pi, fname := range sockfiles {
proto := protos[pi]
f, err := os.Open(fname)
if err != nil {
return nil, fmt.Errorf("%s: %s", fname, err)
}
defer f.Close()
r := bufio.NewReader(f)
// skip header row
_, err = r.ReadString('\n')
if err != nil {
return nil, err
}
for err == nil {
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
// sl local rem ... inode
words := strings.Fields(line)
local := words[1]
rem := words[2]
inode := words[9]
if rem != "00000000:0000" {
// not a "listener" port
continue
}
portv, err := strconv.ParseUint(local[9:], 16, 16)
if err != nil {
return nil, fmt.Errorf("%#v: %s", local[9:], err)
}
inodev := fmt.Sprintf("socket:[%s]", inode)
l = append(l, Port{
Proto: proto,
Port: uint16(portv),
inode: inodev,
})
}
}
sort.Slice(l, func(i, j int) bool {
return (&l[i]).lessThan(&l[j])
})
return l, nil
}
func addProcesses(pl []Port) ([]Port, error) {
pm := map[string]*Port{}
for k := range pl {
pm[pl[k].inode] = &pl[k]
}
pdir, err := os.Open("/proc")
if err != nil {
return nil, fmt.Errorf("/proc: %s", err)
}
defer pdir.Close()
for {
pids, err := pdir.Readdirnames(100)
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("/proc: %s", err)
}
for _, pid := range pids {
_, err := strconv.ParseInt(pid, 10, 64)
if err != nil {
// not a pid, ignore it.
// /proc has lots of non-pid stuff in it.
continue
}
fddir, err := os.Open(fmt.Sprintf("/proc/%s/fd", pid))
if err != nil {
// Can't open fd list for this pid. Maybe
// don't have access. Ignore it.
continue
}
defer fddir.Close()
for {
fds, err := fddir.Readdirnames(100)
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("readdir: %s", err)
}
for _, fd := range fds {
target, err := os.Readlink(fmt.Sprintf("/proc/%s/fd/%s", pid, fd))
if err != nil {
// Not a symlink or no permission.
// Skip it.
continue
}
// TODO(apenwarr): use /proc/*/cmdline instead of /comm?
// Unsure right now whether users will want the extra detail
// or not.
pe := pm[target]
if pe != nil {
comm, err := ioutil.ReadFile(fmt.Sprintf("/proc/%s/comm", pid))
if err != nil {
// Usually shouldn't happen. One possibility is
// the process has gone away, so let's skip it.
continue
}
pe.Process = strings.TrimSpace(string(comm))
}
}
}
}
}
return pl, nil
}

@ -0,0 +1,20 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows,!darwin
package portlist
// We have to run netstat, which is a bit expensive, so don't do it too often.
const POLL_SECONDS = 5
func listPorts() (List, error) {
return listPortsNetstat("-na")
}
func addProcesses(pl []Port) ([]Port, error) {
// Generic version has no way to get process mappings.
// This has to be OS-specific.
return pl, nil
}

@ -0,0 +1,16 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
// Forking on Windows is insanely expensive, so don't do it too often.
const POLL_SECONDS = 5
func listPorts() (List, error) {
return listPortsNetstat("-na")
}
func addProcesses(pl []Port) ([]Port, error) {
return listPortsNetstat("-nab")
}

@ -0,0 +1,78 @@
// 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 ratelimit
import (
"sync"
"time"
)
type Bucket struct {
mu sync.Mutex
FillInterval time.Duration
Burst int
v int
quitCh chan struct{}
started bool
closed bool
}
func (b *Bucket) startLocked() {
b.v = b.Burst
b.quitCh = make(chan struct{})
b.started = true
t := time.NewTicker(b.FillInterval)
go func() {
for {
select {
case <-b.quitCh:
return
case <-t.C:
b.tick()
}
}
}()
}
func (b *Bucket) tick() {
b.mu.Lock()
defer b.mu.Unlock()
if b.v < b.Burst {
b.v++
}
}
func (b *Bucket) Close() {
b.mu.Lock()
if !b.started {
b.closed = true
b.mu.Unlock()
return
}
if b.closed {
b.mu.Unlock()
return
}
b.closed = true
b.mu.Unlock()
b.quitCh <- struct{}{}
}
func (b *Bucket) TryGet() int {
b.mu.Lock()
defer b.mu.Unlock()
if !b.started {
b.startLocked()
}
if b.v > 0 {
b.v--
return b.v + 1
}
return 0
}

@ -0,0 +1,28 @@
// 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 ratelimit
import (
"testing"
"time"
)
func TestBucket(t *testing.T) {
b := Bucket{
FillInterval: time.Second,
Burst: 3,
}
expect := []int{3, 2, 1, 0, 0}
for i, want := range expect {
got := b.TryGet()
if want != got {
t.Errorf("#%d want=%d got=%d\n", i, want, got)
}
}
b.tick()
if want, got := 1, b.TryGet(); want != got {
t.Errorf("after tick: want=%d got=%d\n", want, got)
}
}

@ -0,0 +1,63 @@
// 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 safesocket
import (
"fmt"
"testing"
)
func TestBasics(t *testing.T) {
fmt.Printf("listening2...\n")
l, port, err := Listen("COOKIE", "Tailscale", "test", 0)
if err != nil {
t.Fatal(err)
}
fmt.Printf("listened.\n")
go func() {
fmt.Printf("accepting...\n")
s, err := l.Accept()
if err != nil {
t.Fatal(err)
}
fmt.Printf("accepted.\n")
l.Close()
s.Write([]byte("hello"))
fmt.Printf("server wrote.\n")
b := make([]byte, 1024)
n, err := s.Read(b)
if err != nil {
t.Fatal(err)
}
fmt.Printf("server read %d bytes.\n", n)
if string(b[:n]) != "world" {
t.Fatalf("got %#v, expected %#v\n", string(b[:n]), "world")
}
s.Close()
}()
fmt.Printf("connecting...\n")
c, err := Connect("COOKIE", "Tailscale", "test", port)
if err != nil {
t.Fatal(err)
}
fmt.Printf("connected.\n")
c.Write([]byte("world"))
fmt.Printf("client wrote.\n")
b := make([]byte, 1024)
n, err := c.Read(b)
if err != nil {
t.Fatal(err)
}
fmt.Printf("client read %d bytes.\n", n)
if string(b[:n]) != "hello" {
t.Fatalf("got %#v, expected %#v\n", string(b[:n]), "hello")
}
c.Close()
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save