summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorsalman <salman@tailscale.com>2022-11-05 15:58:02 +0000
committersalman <salman@tailscale.com>2023-06-28 21:11:28 +0100
commit1693329169db77081384a7fb87ffc2bf6b10a638 (patch)
treeb2ca625b60362c76f30677f0a03ba76938c26bd2
parent8e840489ed0409abdcaa3b1942f379a9e6e05625 (diff)
downloadtailscale-s/tsnetd.tar.xz
tailscale-s/tsnetd.zip
cmd/tsnetd: tsnet-based tcp proxys/tsnetd
Signed-off-by: salman <salman@tailscale.com>
-rw-r--r--cmd/tsmultiserve/main.go176
1 files changed, 176 insertions, 0 deletions
diff --git a/cmd/tsmultiserve/main.go b/cmd/tsmultiserve/main.go
new file mode 100644
index 000000000..8c0a927a4
--- /dev/null
+++ b/cmd/tsmultiserve/main.go
@@ -0,0 +1,176 @@
+// Server tsmultiserve is a TCP proxy that can register and listen on multiple tailscale addresses
+// using tsnet.
+//
+// The main motivation for this is to run multiple services on the same host, but give
+// them memorable names and use canonical ports.
+//
+// Usage:
+//
+// tsmultiserve [ts-node:ts-port:dst-host:dst-port ...]
+//
+// For example:
+//
+// tsmultiserve cameras:http:localhost:8001 cameras:rtsp:localhost:rtsp phone:sip:localhost:sip
+//
+// This will register two nodes on your tailnet, "cameras" and "phone". On cameras it will forward
+// port 80 to localhost:8001 and port 554 (rtsp) to localhost:554, and on phone it will forward port
+// 5060 (sip) to localhost:5060.
+//
+// You can get the same effect if you:
+// 1. Run multiple tailscaleds in separate network namespaces or containers, but that can get complicated.
+// 2. Use the caddy-tailscale extension, but that's HTTP only.
+// 2. Use an HTTP proxy & vitual hosts, but now you have to set your own DNS. Also HTTP (or TLS) only.
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "tailscale.com/ipn"
+ "tailscale.com/tsnet"
+)
+
+var logf = func(string, ...any) {}
+
+var (
+ statedir string
+ verbose bool
+)
+
+func authkeyForHost(h string) string {
+ authkey := os.Getenv("TS_AUTHKEY_" + h)
+ if authkey != "" {
+ log.Printf("%v: using authkey from $TS_AUTHKEY_%s", h, h)
+ return authkey
+ }
+ authkey = os.Getenv("TS_AUTHKEY")
+ if authkey != "" {
+ log.Printf("%v: using authkey from $TS_AUTHKEY", h)
+ return authkey
+ }
+ return ""
+}
+
+func up(node string, cfg *ipn.ServeConfig) {
+ ctx := context.Background()
+ statedir := filepath.Join(statedir, node)
+
+ err := os.MkdirAll(statedir, 0770)
+ if err != nil {
+ log.Fatalf("%v: could not make state directory (%s): %v", node, statedir, err)
+ }
+ srv := &tsnet.Server{
+ Hostname: node,
+ Dir: statedir,
+ Logf: logf,
+ AuthKey: authkeyForHost(node),
+ }
+ defer srv.Close()
+
+ lc, err := srv.LocalClient()
+ if err != nil {
+ log.Fatalf("%v: could not get local client: %v", node, err)
+ }
+
+ watcher, err := lc.WatchIPNBus(ctx, ipn.NotifyWatchEngineUpdates|ipn.NotifyInitialState|ipn.NotifyNoPrivateKeys)
+ if err != nil {
+ log.Fatalf("%v: %v", node, err)
+ }
+ defer watcher.Close()
+login:
+ for {
+ n, err := watcher.Next()
+ if err != nil {
+ log.Fatalf("%v: %v", node, err)
+ }
+ if n.ErrMessage != nil {
+ log.Fatalf("%v: %v", node, err)
+ }
+ if state := n.State; state != nil {
+ switch *state {
+ case ipn.Running:
+ break login
+ case ipn.NeedsLogin:
+ if srv.AuthKey == "" {
+ status, err := lc.Status(ctx)
+ if err != nil {
+ log.Fatalf("%v: %v", node, err)
+ }
+ // TODO figure out why this doesn't work without polling. AuthURL isn't always set
+ // immediately after NeedsLogin, possibly a race?
+ for status.AuthURL == "" {
+ time.Sleep(100 * time.Millisecond)
+ status, err = lc.Status(ctx)
+ if err != nil {
+ log.Fatalf("%v: %v", node, err)
+ }
+ }
+ log.Printf("%v login: %s", node, status.AuthURL)
+ }
+ }
+ }
+ }
+
+ err = lc.SetServeConfig(ctx, cfg)
+ if err != nil {
+ log.Fatalf("%v: could not set serve config: %v", node, err)
+ }
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "usage:\n")
+ fmt.Fprintf(os.Stderr, "\ttsmultiserve tailscale-host:tailscale-port:target-host:target-port ...\n\n")
+ fmt.Fprintf(os.Stderr, "flags:\n")
+ flag.PrintDefaults()
+ os.Exit(2)
+}
+
+func main() {
+ log.SetFlags(0)
+ flag.Usage = usage
+ configdir, _ := os.UserConfigDir() // Ignore error. Empty string means we fall back to current directory.
+ flag.StringVar(&statedir, "state-dir", filepath.Join(configdir, "tsmultiserve"), "directory to keep tailscale state")
+ flag.BoolVar(&verbose, "verbose", false, "be verbose")
+ flag.Parse()
+
+ if verbose {
+ logf = log.Printf
+ }
+
+ if flag.NArg() == 0 {
+ usage()
+ }
+
+ nodes := map[string]*ipn.ServeConfig{}
+ for _, arg := range flag.Args() {
+ parts := strings.Split(arg, ":")
+ if len(parts) != 4 {
+ log.Fatalf("could not parse proxy directive")
+ }
+ host, port, dst := parts[0], parts[1], parts[2]+":"+parts[3]
+
+ p, err := net.LookupPort("tcp", port)
+ if err != nil {
+ log.Fatalf("could not lookup port (%v): %v", port, err)
+ }
+
+ if nodes[host] == nil {
+ nodes[host] = &ipn.ServeConfig{TCP: map[uint16]*ipn.TCPPortHandler{}}
+ }
+
+ nodes[host].TCP[uint16(p)] = &ipn.TCPPortHandler{TCPForward: dst}
+ }
+
+ for n, cfg := range nodes {
+ up(n, cfg)
+ }
+
+ select {}
+}