Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
1693329169 |
|
@ -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 {}
|
||||
}
|
Loading…
Reference in New Issue