mirror of https://github.com/tailscale/tailscale/
Move Linux client & common packages into a public repo.
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 @@
|
||||
9
|
@ -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…
Reference in New Issue