Move Linux client & common packages into a public repo.

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

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

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

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

@ -0,0 +1,28 @@
// Copyright 2019 Tailscale & AUTHORS. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package atomicfile contains code related to writing to filesystems
// atomically.
//
// This package should be considered internal; its API is not stable.
package atomicfile // import "tailscale.com/atomicfile"
import (
"fmt"
"io/ioutil"
"os"
)
// WriteFile writes data to filename+some suffix, then renames it
// into filename.
func WriteFile(filename string, data []byte, perm os.FileMode) error {
tmpname := filename + ".new.tmp"
if err := ioutil.WriteFile(tmpname, data, perm); err != nil {
return fmt.Errorf("%#v: %v", tmpname, err)
}
if err := os.Rename(tmpname, filename); err != nil {
return fmt.Errorf("%#v->%#v: %v", tmpname, filename, err)
}
return nil
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1 @@
/relaynode

@ -0,0 +1,17 @@
# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# Build with: docker build -t tailcontrol-alpine .
# Run with: docker run --cap-add=NET_ADMIN --device=/dev/net/tun:/dev/net/tun -it tailcontrol-alpine
FROM debian:stretch-slim
RUN apt-get update && apt-get -y install iproute2 iptables
RUN apt-get -y install ca-certificates
RUN apt-get -y install nginx-light
COPY relaynode /
# tailcontrol -tun=wg0 -dbdir=$HOME/taildb >> tailcontrol.log 2>&1 &
CMD ["/relaynode", "-R", "--config", "relay.conf"]

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

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

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

@ -0,0 +1,10 @@
#!/bin/sh
# Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
set -e
redo-ifchange build
docker run --cap-add=NET_ADMIN \
--device=/dev/net/tun:/dev/net/tun \
-it tailscale

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

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

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

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

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

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

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

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

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

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

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