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