mirror of https://github.com/tailscale/tailscale/
Move Linux client & common packages into a public repo.
parent
c955043dfe
commit
a8d8b8719a
@ -0,0 +1,17 @@
|
||||
# This is the official list of Tailscale
|
||||
# authors for copyright purposes.
|
||||
#
|
||||
# Names should be added to this file as one of
|
||||
# Organization's name
|
||||
# Individual's name <submission email address>
|
||||
# Individual's name <submission email address> <email2> <emailN>
|
||||
#
|
||||
# Please keep the list sorted.
|
||||
#
|
||||
# You do not need to add entries to this list, and we don't actively
|
||||
# populate this list. If you do want to be acknowledged explicitly as
|
||||
# a copyright holder, though, then please send a PR referencing your
|
||||
# earlier contributions and clarifying whether it's you or your
|
||||
# company that owns the rights to your contribution.
|
||||
|
||||
Tailscale Inc.
|
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Tailscale Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,24 @@
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Tailscale Inc. as part of the Tailscale project.
|
||||
|
||||
Tailscale Inc. hereby grants to You a perpetual, worldwide,
|
||||
non-exclusive, no-charge, royalty-free, irrevocable (except as stated
|
||||
in this section) patent license to make, have made, use, offer to
|
||||
sell, sell, import, transfer and otherwise run, modify and propagate
|
||||
the contents of this implementation of Tailscale, where such license
|
||||
applies only to those patent claims, both currently owned or
|
||||
controlled by Tailscale Inc. and acquired in the future, licensable
|
||||
by Tailscale Inc. that are necessarily infringed by this
|
||||
implementation of Tailscale. This grant does not include claims that
|
||||
would be infringed only as a consequence of further modification of
|
||||
this implementation. If you or your agent or exclusive licensee
|
||||
institute or order or agree to the institution of patent litigation
|
||||
against any entity (including a cross-claim or counterclaim in a
|
||||
lawsuit) alleging that this implementation of Tailscale or any code
|
||||
incorporated within this implementation of Tailscale constitutes
|
||||
direct or contributory patent infringement, or inducement of patent
|
||||
infringement, then any patent rights granted to you under this License
|
||||
for this implementation of Tailscale shall terminate as of the date
|
||||
such litigation is filed.
|
@ -0,0 +1,28 @@
|
||||
// Copyright 2019 Tailscale & AUTHORS. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package atomicfile contains code related to writing to filesystems
|
||||
// atomically.
|
||||
//
|
||||
// This package should be considered internal; its API is not stable.
|
||||
package atomicfile // import "tailscale.com/atomicfile"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// WriteFile writes data to filename+some suffix, then renames it
|
||||
// into filename.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
tmpname := filename + ".new.tmp"
|
||||
if err := ioutil.WriteFile(tmpname, data, perm); err != nil {
|
||||
return fmt.Errorf("%#v: %v", tmpname, err)
|
||||
}
|
||||
if err := os.Rename(tmpname, filename); err != nil {
|
||||
return fmt.Errorf("%#v->%#v: %v", tmpname, filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
/*.tar.gz
|
||||
/*.deb
|
||||
/*.rpm
|
||||
/*.spec
|
||||
/pkgver
|
||||
debian/changelog
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/*.log
|
||||
debian/*.substvars
|
||||
debian/*.debhelper
|
||||
debian/tailscale-relay
|
||||
/tailscale-relay/
|
||||
/tailscale-relay-*
|
@ -0,0 +1,63 @@
|
||||
{
|
||||
// Declare static groups of users beyond those in the identity service
|
||||
"Groups": {
|
||||
"group:eng": ["u1@example.com", "u2@example.com"]
|
||||
},
|
||||
|
||||
// Declare convenient hostname aliases to use in place of IP addresses
|
||||
"Hosts": {
|
||||
"h222": "100.2.2.2"
|
||||
},
|
||||
|
||||
// Access control list
|
||||
"ACLs": [
|
||||
{
|
||||
"Action": "accept",
|
||||
// Match any of several users
|
||||
"Users": ["a@example.com", "b@example.com"],
|
||||
// Match any port on h222, and port 22 of 10.1.2.3
|
||||
"Ports": ["h222:*", "10.1.2.3:22"]
|
||||
},
|
||||
{
|
||||
"Action": "accept",
|
||||
// Match any user at all
|
||||
"Users": ["*"],
|
||||
// Match port 80 on one machine, ports 53 and 5353 on a second one,
|
||||
// and ports 8000 through 8080 (a port range) on a third one.
|
||||
"Ports": ["h222:80", "10.8.8.8:53,5353", "10.2.3.4:8000-8080"]
|
||||
},
|
||||
{
|
||||
"Action": "accept",
|
||||
// Match all users in the "Admin" role (network administrators)
|
||||
"Users": ["role:Admin", "group:eng"],
|
||||
// Allow access to port 22 on all servers
|
||||
"Ports": ["*:22"]
|
||||
},
|
||||
{
|
||||
"Action": "accept",
|
||||
"Users": ["role:User"],
|
||||
// Match only windows and linux workstations (not implemented yet)
|
||||
"OS": ["windows", "linux"],
|
||||
// Only desktop machines are allowed to access this server
|
||||
"Ports": ["10.1.1.1:443"]
|
||||
},
|
||||
{
|
||||
"Action": "accept",
|
||||
"Users": ["*"],
|
||||
// Match machines which have never been authorized, or which expired.
|
||||
// (not implemented yet)
|
||||
"MachineAuth": ["unauthorized", "expired"],
|
||||
// Logged-in users on unauthorized machines can access the email server.
|
||||
// Open the TLS ports for SMTP, IMAP, and HTTP.
|
||||
"Ports": ["10.1.2.3:465", "10.1.2.3:993", "10.1.2.3:443"]
|
||||
},
|
||||
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// the above ACLs to apply.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
|
||||
// Leave this line here so that every rule can end in a comma.
|
||||
// It has no effect since it has no matching rules.
|
||||
{"Action": "accept"}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
rm -f debian/changelog *~ debian/*~
|
@ -0,0 +1,13 @@
|
||||
exec >&2
|
||||
read -r package <package
|
||||
rm -f *~ .*~ \
|
||||
debian/*~ debian/changelog debian/debhelper-build-stamp \
|
||||
debian/*.log debian/files debian/*.substvars debian/*.debhelper \
|
||||
*.tar.gz *.deb *.rpm *.spec pkgver relaynode *.exe
|
||||
[ -n "$package" ] && rm -rf "debian/$package"
|
||||
for d in */.stamp; do
|
||||
if [ -e "$d" ]; then
|
||||
dir=$(dirname "$d")
|
||||
rm -rf "$dir"
|
||||
fi
|
||||
done
|
@ -0,0 +1,10 @@
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r version <"$S/oss/version/short.txt"
|
||||
arch=$(dpkg --print-architecture)
|
||||
|
||||
redo-ifchange "$dir/${package}_$arch.deb"
|
||||
rm -f "$dir/${package}"_*_"$arch.deb"
|
||||
ln -sf "${package}_$arch.deb" "$dir/${package}_${version}_$arch.deb"
|
@ -0,0 +1 @@
|
||||
Tailscale IPN relay daemon.
|
@ -0,0 +1,5 @@
|
||||
redo-ifchange ../../../version/short.txt gen-changelog
|
||||
(
|
||||
cd ..
|
||||
debian/gen-changelog
|
||||
) >$3
|
@ -0,0 +1 @@
|
||||
9
|
@ -0,0 +1,14 @@
|
||||
Source: tailscale-relay
|
||||
Section: net
|
||||
Priority: extra
|
||||
Maintainer: Avery Pennarun <apenwarr@tailscale.com>
|
||||
Build-Depends: debhelper (>= 10.2.5), dh-systemd (>= 1.5)
|
||||
Standards-Version: 3.9.2
|
||||
Homepage: https://tailscale.com/
|
||||
Vcs-Git: https://github.com/tailscale/tailscale
|
||||
Vcs-Browser: https://github.com/tailscale/tailscale
|
||||
|
||||
Package: tailscale-relay
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Description: Traffic relay node for Tailscale IPN
|
@ -0,0 +1,11 @@
|
||||
Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173
|
||||
Upstream-Name: tailscale-relay
|
||||
Upstream-Contact: Avery Pennarun <apenwarr@tailscale.com>
|
||||
Source: https://github.com/tailscale/tailscale/
|
||||
|
||||
Files: *
|
||||
Copyright: © 2019 Tailscale Inc. <info@tailscale.com>
|
||||
License: Proprietary
|
||||
*
|
||||
* Copyright 2019 Tailscale Inc. All rights reserved.
|
||||
*
|
@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
read junk pkgname <debian/control
|
||||
read shortver <../../version/short.txt
|
||||
git log --pretty='format:'"$pkgname"' (SHA:%H) unstable; urgency=low
|
||||
|
||||
* %s
|
||||
|
||||
-- %aN <%aE> %aD
|
||||
' . |
|
||||
python -Sc '
|
||||
import os, re, subprocess, sys
|
||||
|
||||
first = True
|
||||
def Describe(g):
|
||||
global first
|
||||
if first:
|
||||
s = sys.argv[1]
|
||||
first = False
|
||||
else:
|
||||
sha = g.group(1)
|
||||
s = subprocess.check_output(["git", "describe", "--", sha]).strip().decode("utf-8")
|
||||
return re.sub(r"^\D*", "", s)
|
||||
|
||||
print(re.sub(r"SHA:([0-9a-f]+)", Describe, sys.stdin.read()))
|
||||
' "$shortver"
|
@ -0,0 +1,4 @@
|
||||
relaynode /usr/sbin
|
||||
tailscale-login /usr/sbin
|
||||
taillogin /usr/sbin
|
||||
acl.json /etc/tailscale
|
@ -0,0 +1,8 @@
|
||||
#DEBHELPER#
|
||||
|
||||
f=/var/lib/tailscale/relay.conf
|
||||
if ! [ -e "$f" ]; then
|
||||
echo
|
||||
echo "Note: Run tailscale-login to configure $f." >&2
|
||||
echo
|
||||
fi
|
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/make -f
|
||||
DESTDIR=debian/tailscale-relay
|
||||
|
||||
override_dh_auto_test:
|
||||
override_dh_auto_install:
|
||||
mkdir -p "${DESTDIR}/etc/default"
|
||||
cp tailscale-relay.defaults "${DESTDIR}/etc/default/tailscale-relay"
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd
|
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Traffic relay node for Tailscale IPN
|
||||
After=network.target
|
||||
ConditionPathExists=/var/lib/tailscale/relay.conf
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscale-relay
|
||||
ExecStart=/usr/sbin/relaynode --config=/var/lib/tailscale/relay.conf --tun=wg0 $PORT $ACL_FILE $FLAGS
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -0,0 +1,20 @@
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
redo-ifchange "$S/oss/version/short.txt" "$S/$dir/package" "$dir/debtmp.dir"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r version <"$S/oss/version/short.txt"
|
||||
arch=$(dpkg --print-architecture)
|
||||
|
||||
(
|
||||
cd "$S/$dir"
|
||||
git ls-files debian | xargs redo-ifchange debian/changelog
|
||||
)
|
||||
cp -a "$S/$dir/debian" "$dir/debtmp/"
|
||||
rm -f "$dir/debtmp/debian/$package.debhelper.log"
|
||||
(
|
||||
cd "$dir/debtmp" &&
|
||||
debian/rules build &&
|
||||
fakeroot debian/rules binary
|
||||
)
|
||||
|
||||
mv "$dir/${package}_${version}_${arch}.deb" "$3"
|
@ -0,0 +1,21 @@
|
||||
# Generate a directory tree suitable for forming a tarball of
|
||||
# this package.
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
outdir=$PWD/${1%.dir}
|
||||
rm -rf "$outdir"
|
||||
mkdir "$outdir"
|
||||
touch $outdir/.stamp
|
||||
sfiles="
|
||||
tailscale-login
|
||||
acl.json
|
||||
debian/*.service
|
||||
*.defaults
|
||||
"
|
||||
ofiles="
|
||||
relaynode
|
||||
../taillogin/taillogin
|
||||
"
|
||||
redo-ifchange "$outdir/.stamp"
|
||||
(cd "$S/$dir" && redo-ifchange $sfiles && cp $sfiles "$outdir/")
|
||||
(cd "$dir" && redo-ifchange $ofiles && cp $ofiles "$outdir/")
|
@ -0,0 +1,14 @@
|
||||
exec >&2
|
||||
dir=${1%/*}
|
||||
pkg=${1##*/}
|
||||
pkg=${pkg%.rpm}
|
||||
redo-ifchange "$S/oss/version/short.txt" "$dir/$pkg.tar.gz" "$dir/$pkg.spec"
|
||||
read -r pkgver junk <"$S/oss/version/short.txt"
|
||||
|
||||
machine=$(uname -m)
|
||||
rpmbase=$HOME/rpmbuild
|
||||
|
||||
mkdir -p "$rpmbase/SOURCES/"
|
||||
cp "$dir/$pkg.tar.gz" "$rpmbase/SOURCES/"
|
||||
rpmbuild -bb "$dir/$pkg.spec"
|
||||
mv "$rpmbase/RPMS/$machine/$pkg-$pkgver.$machine.rpm" $3
|
@ -0,0 +1,7 @@
|
||||
redo-ifchange "$S/$1.in" "$S/oss/version/short.txt"
|
||||
read -r pkgver junk <"$S/oss/version/short.txt"
|
||||
basever=${pkgver%-*}
|
||||
subver=${pkgver#*-}
|
||||
sed -e "s/Version: 0.00$/Version: $basever/" \
|
||||
-e "s/Release: 0$/Release: $subver/" \
|
||||
<"$S/$1.in" >"$3"
|
@ -0,0 +1,8 @@
|
||||
exec >&2
|
||||
xdir=${1%.tar.gz}
|
||||
base=${xdir##*/}
|
||||
updir=${xdir%/*}
|
||||
redo-ifchange "$xdir.dir"
|
||||
OUT="$PWD/$3"
|
||||
|
||||
cd "$updir" && tar -czvf "$OUT" --exclude "$base/.stamp" "$base"
|
@ -0,0 +1,15 @@
|
||||
# Build packages for customer distribution.
|
||||
dir=${1%/*}
|
||||
cd "$dir"
|
||||
targets="tarball"
|
||||
if which dh_clean fakeroot dpkg >/dev/null; then
|
||||
targets="$targets deb"
|
||||
else
|
||||
echo "Skipping debian packages: debhelper and/or dpkg build tools missing." >&2
|
||||
fi
|
||||
if which rpm >/dev/null; then
|
||||
targets="$targets rpm"
|
||||
else
|
||||
echo "Skipping rpm packages: rpm build tools missing." >&2
|
||||
fi
|
||||
redo-ifchange $targets
|
@ -0,0 +1 @@
|
||||
/relaynode
|
@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
# Build with: docker build -t tailcontrol-alpine .
|
||||
# Run with: docker run --cap-add=NET_ADMIN --device=/dev/net/tun:/dev/net/tun -it tailcontrol-alpine
|
||||
|
||||
FROM debian:stretch-slim
|
||||
|
||||
RUN apt-get update && apt-get -y install iproute2 iptables
|
||||
RUN apt-get -y install ca-certificates
|
||||
RUN apt-get -y install nginx-light
|
||||
|
||||
COPY relaynode /
|
||||
|
||||
# tailcontrol -tun=wg0 -dbdir=$HOME/taildb >> tailcontrol.log 2>&1 &
|
||||
CMD ["/relaynode", "-R", "--config", "relay.conf"]
|
@ -0,0 +1 @@
|
||||
redo-ifchange build
|
@ -0,0 +1,3 @@
|
||||
exec >&2
|
||||
redo-ifchange Dockerfile relaynode
|
||||
docker build -t tailscale .
|
@ -0,0 +1,2 @@
|
||||
redo-ifchange ../relaynode
|
||||
cp ../relaynode $3
|
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
set -e
|
||||
redo-ifchange build
|
||||
docker run --cap-add=NET_ADMIN \
|
||||
--device=/dev/net/tun:/dev/net/tun \
|
||||
-it tailscale
|
@ -0,0 +1 @@
|
||||
tailscale-relay
|
@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Relaynode is the old Linux Tailscale daemon.
|
||||
//
|
||||
// Deprecated: this program will be soon deleted. The replacement is
|
||||
// cmd/tailscaled.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/control/policy"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
config := getopt.StringLong("config", 'f', "", "path to config file")
|
||||
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
|
||||
listenport := getopt.Uint16Long("port", 'p', magicsock.DefaultPort, "WireGuard port (0=autoselect)")
|
||||
tunname := getopt.StringLong("tun", 0, "wg0", "tunnel interface name")
|
||||
alwaysrefresh := getopt.BoolLong("always-refresh", 0, "force key refresh at startup")
|
||||
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
|
||||
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
|
||||
rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
|
||||
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
|
||||
routes := getopt.StringLong("routes", 0, "", "list of IP ranges this node can relay")
|
||||
aclfile := getopt.StringLong("acl-file", 0, "", "restrict traffic relaying according to json ACL file")
|
||||
derp := getopt.BoolLong("derp", 0, "enable bypass via Detour Encrypted Routing Protocol (DERP)", "false")
|
||||
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
}
|
||||
uflags := controlclient.UFlagsHelper(!*nuroutes, *rroutes, *droutes)
|
||||
if *config == "" {
|
||||
log.Fatal("no --config file specified")
|
||||
}
|
||||
if *tunname == "" {
|
||||
log.Printf("Warning: no --tun device specified; routing disabled.\n")
|
||||
}
|
||||
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
|
||||
// The wgengine takes a wireguard configuration produced by the
|
||||
// controlclient, and runs the actual tunnels and packets.
|
||||
var e wgengine.Engine
|
||||
if *fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, *listenport, *derp)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport, *derp)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting wireguard engine: %v\n", err)
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
var lastacljson string
|
||||
var p *policy.Policy
|
||||
|
||||
if *aclfile == "" {
|
||||
e.SetFilter(nil)
|
||||
} else {
|
||||
lastacljson = readOrFatal(*aclfile)
|
||||
p = installFilterOrFatal(e, *aclfile, lastacljson, nil)
|
||||
}
|
||||
|
||||
var lastNetMap *controlclient.NetworkMap
|
||||
var lastUserMap map[string][]filter.IP
|
||||
statusFunc := func(new controlclient.Status) {
|
||||
if new.URL != "" {
|
||||
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
|
||||
return
|
||||
}
|
||||
if new.Err != "" {
|
||||
log.Print(new.Err)
|
||||
return
|
||||
}
|
||||
if new.Persist != nil {
|
||||
if err := saveConfig(*config, *new.Persist); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
if m := new.NetMap; m != nil {
|
||||
if lastNetMap != nil {
|
||||
s1 := strings.Split(lastNetMap.Concise(), "\n")
|
||||
s2 := strings.Split(new.NetMap.Concise(), "\n")
|
||||
logf("netmap diff:\n%v\n", cmp.Diff(s1, s2))
|
||||
}
|
||||
lastNetMap = m
|
||||
|
||||
if m.Equal(&controlclient.NetworkMap{}) {
|
||||
return
|
||||
}
|
||||
|
||||
wgcfg, err := m.WGCfg(uflags, m.DNS)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting wg config: %v\n", err)
|
||||
}
|
||||
err = e.Reconfig(wgcfg, m.DNSDomains)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reconfiguring engine: %v\n", err)
|
||||
}
|
||||
lastUserMap = m.UserMap()
|
||||
if p != nil {
|
||||
matches, err := p.Expand(lastUserMap)
|
||||
if err != nil {
|
||||
log.Fatalf("Error expanding ACLs: %v\n", err)
|
||||
}
|
||||
e.SetFilter(filter.New(matches))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(*config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hi := controlclient.NewHostinfo()
|
||||
hi.FrontendLogID = pol.PublicID.String()
|
||||
hi.BackendLogID = pol.PublicID.String()
|
||||
if *routes != "" {
|
||||
for _, routeStr := range strings.Split(*routes, ",") {
|
||||
cidr, err := wgcfg.ParseCIDR(routeStr)
|
||||
if err != nil {
|
||||
log.Fatalf("--routes: not an IP range: %s", routeStr)
|
||||
}
|
||||
hi.RoutableIPs = append(hi.RoutableIPs, *cidr)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := controlclient.New(controlclient.Options{
|
||||
Persist: cfg,
|
||||
ServerURL: *server,
|
||||
Hostinfo: &hi,
|
||||
NewDecompressor: func() (controlclient.Decompressor, error) {
|
||||
return zstd.NewReader(nil)
|
||||
},
|
||||
KeepAlive: true,
|
||||
})
|
||||
c.SetStatusFunc(statusFunc)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
lf := controlclient.LoginDefault
|
||||
if *alwaysrefresh {
|
||||
lf |= controlclient.LoginInteractive
|
||||
}
|
||||
c.Login(nil, lf)
|
||||
|
||||
// Print the wireguard status when we get an update.
|
||||
e.SetStatusCallback(func(s *wgengine.Status, err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("Wireguard engine status error: %v\n", err)
|
||||
}
|
||||
var ss []string
|
||||
for _, p := range s.Peers {
|
||||
if p.LastHandshake.IsZero() {
|
||||
ss = append(ss, "x")
|
||||
} else {
|
||||
ss = append(ss, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
|
||||
}
|
||||
}
|
||||
logf("v%v peers: %v\n", version.LONG, strings.Join(ss, " "))
|
||||
c.UpdateEndpoints(0, s.LocalAddrs)
|
||||
})
|
||||
|
||||
if *debug != "" {
|
||||
go runDebugServer(*debug)
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
signal.Notify(sigCh, syscall.SIGTERM)
|
||||
|
||||
t := time.NewTicker(5 * time.Second)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
// For the sake of curiosity, request a status
|
||||
// update periodically.
|
||||
e.RequestStatus()
|
||||
|
||||
// check if aclfile has changed.
|
||||
// TODO(apenwarr): use fsnotify instead of polling?
|
||||
if *aclfile != "" {
|
||||
json := readOrFatal(*aclfile)
|
||||
if json != lastacljson {
|
||||
logf("ACL file (%v) changed. Reloading filter.\n", *aclfile)
|
||||
lastacljson = json
|
||||
p = installFilterOrFatal(e, *aclfile, json, lastUserMap)
|
||||
}
|
||||
}
|
||||
case <-sigCh:
|
||||
logf("signal received, exiting")
|
||||
t.Stop()
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
e.Close()
|
||||
pol.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func loadConfig(path string) (cfg controlclient.Persist, err error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("config %s does not exist", path)
|
||||
return controlclient.Persist{}, nil
|
||||
}
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfig(path string, cfg controlclient.Persist) error {
|
||||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readOrFatal(filename string) string {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Fatalf("%v: ReadFile: %v\n", filename, err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func installFilterOrFatal(e wgengine.Engine, filename, acljson string, usermap map[string][]filter.IP) *policy.Policy {
|
||||
p, err := policy.Parse(acljson)
|
||||
if err != nil {
|
||||
log.Fatalf("%v: json filter: %v\n", filename, err)
|
||||
}
|
||||
|
||||
matches, err := p.Expand(usermap)
|
||||
if err != nil {
|
||||
log.Fatalf("%v: json filter: %v\n", filename, err)
|
||||
}
|
||||
|
||||
e.SetFilter(filter.New(matches))
|
||||
return p
|
||||
}
|
||||
|
||||
func runDebugServer(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
srv := http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
exec >&2
|
||||
dir=${2%/*}
|
||||
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r pkgver <"$S/oss/version/short.txt"
|
||||
machine=$(uname -m)
|
||||
redo-ifchange "$dir/$package.rpm"
|
||||
rm -f "$dir/${package}"-*."$machine.rpm"
|
||||
ln -sf "$package.rpm" "$dir/$package-$pkgver.$machine.rpm"
|
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
cfg=/var/lib/tailscale/relay.conf
|
||||
dir=$(dirname "$0")
|
||||
"$dir/taillogin" --config="$cfg"
|
@ -0,0 +1,14 @@
|
||||
# Set the port to listen on for incoming VPN packets.
|
||||
# Remote nodes will automatically be informed about the new port number,
|
||||
# but you might want to configure this in order to set external firewall
|
||||
# settings.
|
||||
PORT="--port=41641"
|
||||
|
||||
# Comment out this line to allow all traffic to be relayed.
|
||||
# Or edit the given file to allow specific traffic.
|
||||
# The example file is unlikely to match any users on your network, so it
|
||||
# will block all incoming traffic by default.
|
||||
ACL_FILE="--acl-file=/etc/tailscale/acl.json"
|
||||
|
||||
# Extra flags you might want to pass to relaynode.
|
||||
FLAGS=""
|
@ -0,0 +1,42 @@
|
||||
Name: tailscale-relay
|
||||
Version: 0.00
|
||||
Release: 0
|
||||
Summary: Traffic relay node for Tailscale
|
||||
Group: Network
|
||||
License: Proprietary
|
||||
URL: https://tailscale.com/
|
||||
Vendor: Tailscale Inc.
|
||||
#Source: https://github.com/tailscale/tailscale
|
||||
Source0: tailscale-relay.tar.gz
|
||||
#Prefix: %{_prefix}
|
||||
Packager: Avery Pennarun <apenwarr@tailscale.com>
|
||||
BuildRoot: %{_tmppath}/%{name}-root
|
||||
|
||||
%description
|
||||
Traffic relay node for Tailscale.
|
||||
|
||||
%prep
|
||||
%setup -n tailscale-relay
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
D=$RPM_BUILD_ROOT
|
||||
[ "$D" = "/" -o -z "$D" ] && exit 99
|
||||
rm -rf "$D"
|
||||
mkdir -p $D/usr/sbin $D/lib/systemd/system $D/etc/default $D/etc/tailscale
|
||||
cp taillogin tailscale-login relaynode $D/usr/sbin
|
||||
cp tailscale-relay.service $D/lib/systemd/system/
|
||||
cp tailscale-relay.defaults $D/etc/default/tailscale-relay
|
||||
cp acl.json $D/etc/tailscale/acl.json
|
||||
|
||||
%clean
|
||||
|
||||
%files
|
||||
%defattr(-,root,root)
|
||||
%config(noreplace) /etc/default/tailscale-relay
|
||||
%config(noreplace) /etc/tailscale/acl.json
|
||||
/lib/systemd/system/tailscale-relay.service
|
||||
/usr/sbin/taillogin
|
||||
/usr/sbin/tailscale-login
|
||||
/usr/sbin/relaynode
|
@ -0,0 +1,7 @@
|
||||
dir=${1%/*}
|
||||
redo-ifchange "$S/$dir/package" "$S/oss/version/short.txt"
|
||||
read -r package <"$S/$dir/package"
|
||||
read -r version <"$S/oss/version/short.txt"
|
||||
redo-ifchange "$dir/$package.tar.gz"
|
||||
rm -f "$dir/$package"-*.tar.gz
|
||||
ln -sf "$package.tar.gz" "$dir/$package-$version.tar.gz"
|
@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The taillogin command, invoked via the tailscale-login shell script, is shipped
|
||||
// with the current (old) Linux client, to log in to Tailscale on a Linux box.
|
||||
//
|
||||
// Deprecated: this will be deleted, to be replaced by cmd/tailscale.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logpolicy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := getopt.StringLong("config", 'f', "", "path to config file")
|
||||
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailgate server")
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatal("too many non-flag arguments")
|
||||
}
|
||||
if *config == "" {
|
||||
log.Fatal("no --config file specified")
|
||||
}
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
|
||||
defer pol.Close()
|
||||
|
||||
cfg, err := loadConfig(*config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hi := controlclient.NewHostinfo()
|
||||
hi.FrontendLogID = pol.PublicID.String()
|
||||
hi.BackendLogID = pol.PublicID.String()
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
c, err := controlclient.New(controlclient.Options{
|
||||
Persist: cfg,
|
||||
ServerURL: *server,
|
||||
Hostinfo: &hi,
|
||||
})
|
||||
c.SetStatusFunc(func(new controlclient.Status) {
|
||||
if new.URL != "" {
|
||||
fmt.Fprintf(os.Stderr, "To authenticate, visit:\n\n\t%s\n\n", new.URL)
|
||||
return
|
||||
}
|
||||
if new.Err != "" {
|
||||
log.Print(new.Err)
|
||||
return
|
||||
}
|
||||
if new.Persist != nil {
|
||||
if err := saveConfig(*config, *new.Persist); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
if new.NetMap != nil {
|
||||
done <- struct{}{}
|
||||
}
|
||||
})
|
||||
c.Login(nil, 0)
|
||||
<-done
|
||||
log.Printf("Success.\n")
|
||||
}
|
||||
|
||||
func loadConfig(path string) (cfg controlclient.Persist, err error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("config %s does not exist", path)
|
||||
return controlclient.Persist{}, nil
|
||||
}
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
return controlclient.Persist{}, fmt.Errorf("load config: %v", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfig(path string, cfg controlclient.Persist) error {
|
||||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The tailscale command is the Tailscale command-line client. It interacts
|
||||
// with the tailscaled client daemon.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/safesocket"
|
||||
)
|
||||
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, c net.Conn) {
|
||||
defer log.Printf("Control connection done.\n")
|
||||
defer c.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(c)
|
||||
if err != nil {
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
config := getopt.StringLong("config", 'f', "", "path to config file")
|
||||
server := getopt.StringLong("server", 's', "https://login.tailscale.com", "URL to tailcontrol server")
|
||||
alwaysrefresh := getopt.BoolLong("always-refresh", 0, "force key refresh at startup")
|
||||
nuroutes := getopt.BoolLong("no-single-routes", 'N', "disallow (non-subnet) routes to single nodes")
|
||||
rroutes := getopt.BoolLong("remote-routes", 'R', "allow routing subnets to remote nodes")
|
||||
droutes := getopt.BoolLong("default-routes", 'D', "allow default route on remote node")
|
||||
getopt.Parse()
|
||||
if *config == "" {
|
||||
logpolicy.New("tailnode.log.tailscale.io", "tailscale")
|
||||
log.Fatal("no --config file specified")
|
||||
}
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
}
|
||||
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io", *config)
|
||||
defer pol.Close()
|
||||
|
||||
prefs, err := loadConfig(*config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
// TODO(apenwarr): allow setting/using CorpDNS
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = *rroutes || *droutes
|
||||
prefs.AllowSingleHosts = !*nuroutes
|
||||
|
||||
c, err := safesocket.Connect("", "Tailscale", "tailscaled", 41112)
|
||||
if err != nil {
|
||||
log.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
lf := controlclient.LoginDefault
|
||||
if *alwaysrefresh {
|
||||
lf |= controlclient.LoginInteractive
|
||||
}
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-interrupt
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||
opts := ipn.Options{
|
||||
Prefs: prefs,
|
||||
ServerURL: *server,
|
||||
LoginFlags: lf,
|
||||
Notify: func(n ipn.Notify) {
|
||||
log.Printf("Notify: %v\n", n)
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
bc.StartLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", *server)
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
if p := n.Prefs; p != nil {
|
||||
prefs = *p
|
||||
saveConfig(*config, *p)
|
||||
}
|
||||
},
|
||||
}
|
||||
bc.Start(opts)
|
||||
pump(ctx, bc, c)
|
||||
}
|
||||
|
||||
func loadConfig(path string) (ipn.Prefs, error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("config %s does not exist", path)
|
||||
return ipn.NewPrefs(), nil
|
||||
}
|
||||
return ipn.PrefsFromBytes(b, false)
|
||||
}
|
||||
|
||||
func saveConfig(path string, prefs ipn.Prefs) error {
|
||||
b, err := json.MarshalIndent(prefs, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(path, b, 0666); err != nil {
|
||||
return fmt.Errorf("save config: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The tailscaled program is the Tailscale client daemon. It's configured
|
||||
// and controlled via the tailscale CLI program.
|
||||
//
|
||||
// It primarily supports Linux, though other systems will likely be
|
||||
// supported in the future.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fake := getopt.BoolLong("fake", 0, "fake tunnel+routing instead of tuntap")
|
||||
debug := getopt.StringLong("debug", 0, "", "Address of debug server")
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
logf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io", "tailscaled")
|
||||
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
}
|
||||
|
||||
if *debug != "" {
|
||||
go runDebugServer(*debug)
|
||||
}
|
||||
|
||||
var e wgengine.Engine
|
||||
if *fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, false)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, "ts0", 0, false)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("wgengine.New: %v\n", err)
|
||||
}
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
opts := ipnserver.Options{
|
||||
SurviveDisconnects: true,
|
||||
AllowQuit: false,
|
||||
}
|
||||
err = ipnserver.Run(context.Background(), logf, pol.PublicID.String(), opts, e)
|
||||
if err != nil {
|
||||
log.Fatalf("tailscaled: %v\n", err)
|
||||
}
|
||||
|
||||
// TODO(crawshaw): It would be nice to start a timeout context the moment a signal
|
||||
// is received and use that timeout to give us a moment to finish uploading logs
|
||||
// here. But the signal is handled inside ipnserver.Run, so some plumbing is needed.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
pol.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func runDebugServer(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
srv := http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,594 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlclient implements the client for the IPN control plane.
|
||||
//
|
||||
// It handles authentication, port picking, and collects the local
|
||||
// network configuration.
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/logger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// TODO(apenwarr): eliminate the 'state' variable, as it's now obsolete.
|
||||
// It's used only by the unit tests.
|
||||
type state int
|
||||
|
||||
const (
|
||||
stateNew = state(iota)
|
||||
stateNotAuthenticated
|
||||
stateAuthenticating
|
||||
stateURLVisitRequired
|
||||
stateAuthenticated
|
||||
stateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s state) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s state) String() string {
|
||||
switch s {
|
||||
case stateNew:
|
||||
return "state:new"
|
||||
case stateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case stateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case stateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case stateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case stateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
LoginFinished *struct{}
|
||||
Err string
|
||||
URL string
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo tailcfg.Hostinfo // current Hostinfo data
|
||||
state state
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Status) Equal(s2 *Status) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
return s != nil && s2 != nil &&
|
||||
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
|
||||
s.Err == s2.Err &&
|
||||
s.URL == s2.URL &&
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
||||
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
||||
s.state == s2.state
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
b, err := json.MarshalIndent(s, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.state.String() + " " + string(b)
|
||||
}
|
||||
|
||||
type LoginGoal struct {
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
token *oauth2.Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
}
|
||||
|
||||
// Client connects to a tailcontrol server for a node.
|
||||
type Client struct {
|
||||
direct *Direct // our interface to the server APIs
|
||||
timeNow func() time.Time
|
||||
logf logger.Logf
|
||||
expiry *time.Time
|
||||
closed bool
|
||||
newMapCh chan struct{} // readable when we must restart a map request
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state state
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
authCancel func() // cancel the auth context
|
||||
mapCancel func() // cancel the netmap context
|
||||
quit chan struct{} // when closed, goroutines should all exit
|
||||
authDone chan struct{} // when closed, auth goroutine is done
|
||||
mapDone chan struct{} // when closed, map goroutine is done
|
||||
}
|
||||
|
||||
// New creates and starts a new Client.
|
||||
func New(opts Options) (*Client, error) {
|
||||
c, err := NewNoStart(opts)
|
||||
if c != nil {
|
||||
c.Start()
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// NewNoStart creates a new Client, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (*Client, error) {
|
||||
direct, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newMapCh: make(chan struct{}, 1),
|
||||
quit: make(chan struct{}),
|
||||
authDone: make(chan struct{}),
|
||||
mapDone: make(chan struct{}),
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Start starts the client's goroutines.
|
||||
//
|
||||
// It should only be called for clients created by NewNoStart.
|
||||
func (c *Client) Start() {
|
||||
go c.authRoutine()
|
||||
go c.mapRoutine()
|
||||
}
|
||||
|
||||
func (c *Client) cancelAuth() {
|
||||
c.mu.Lock()
|
||||
if c.authCancel != nil {
|
||||
c.authCancel()
|
||||
}
|
||||
if !c.closed {
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) cancelMapLocked() {
|
||||
if c.mapCancel != nil {
|
||||
c.mapCancel()
|
||||
}
|
||||
if !c.closed {
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) cancelMapUnsafely() {
|
||||
c.mu.Lock()
|
||||
c.cancelMapLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("cancelMapSafely: synced=%v\n", c.synced)
|
||||
|
||||
if c.inPollNetMap == true {
|
||||
// received at least one netmap since the last
|
||||
// interruption. That means the server has already
|
||||
// fully processed our last request, which might
|
||||
// include UpdateEndpoints(). Interrupt it and try
|
||||
// again.
|
||||
c.cancelMapLocked()
|
||||
} else {
|
||||
// !synced means we either haven't done a netmap
|
||||
// request yet, or it hasn't answered yet. So the
|
||||
// server is in an undefined state. If we send
|
||||
// another netmap request too soon, it might race
|
||||
// with the last one, and if we're very unlucky,
|
||||
// the new request will be applied before the old one,
|
||||
// and the wrong endpoints will get registered. We
|
||||
// have to tell the client to abort politely, only
|
||||
// after it receives a response to its existing netmap
|
||||
// request.
|
||||
select {
|
||||
case c.newMapCh <- struct{}{}:
|
||||
c.logf("cancelMapSafely: wrote to channel\n")
|
||||
default:
|
||||
// if channel write failed, then there was already
|
||||
// an outstanding newMapCh request. One is enough,
|
||||
// since it'll always use the latest endpoints.
|
||||
c.logf("cancelMapSafely: channel was full\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.Backoff{Name: "authRoutine"}
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("authRoutine: %s\n", c.state)
|
||||
expiry := c.expiry
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
synced := c.synced
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("authRoutine: quit\n")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v\n", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
if ctx.Err() == nil {
|
||||
c.sendStatus("authRoutine1", err, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if goal == nil {
|
||||
// Wait for something interesting to happen
|
||||
var exp <-chan time.Time
|
||||
if expiry != nil && !expiry.IsZero() {
|
||||
// if expiry is in the future, don't delay
|
||||
// past that time.
|
||||
// If it's in the past, then it's already
|
||||
// being handled by someone, so no need to
|
||||
// wake ourselves up again.
|
||||
now := c.timeNow()
|
||||
if expiry.Before(now) {
|
||||
delay := expiry.Sub(now)
|
||||
if delay > 5*time.Second {
|
||||
delay = time.Second
|
||||
}
|
||||
exp = time.After(delay)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logf("authRoutine: context done.\n")
|
||||
case <-exp:
|
||||
// Unfortunately the key expiry isn't provided
|
||||
// by the control server until mapRequest.
|
||||
// So we have to do some hackery with c.expiry
|
||||
// in here.
|
||||
// TODO(apenwarr): add a key expiry field in RegisterResponse.
|
||||
c.logf("authRoutine: key expiration check.\n")
|
||||
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
|
||||
c.logf("Key expired; setting loggedIn=false.")
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: c.loggedIn,
|
||||
}
|
||||
c.loggedIn = false
|
||||
c.expiry = nil
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
} else if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(c.authCtx)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
bo.BackOff(ctx, err)
|
||||