mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
300 lines
8.0 KiB
Go
300 lines
8.0 KiB
Go
// 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 "tailscale.com/cmd/relaynode"
|
|
|
|
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")
|
|
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)
|
|
} else {
|
|
e, err = wgengine.NewUserspaceEngine(logf, *tunname, *listenport)
|
|
}
|
|
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)
|
|
}
|
|
}
|