tsnet.Server
The tsnet
package provides the ability for Go programs to programmatically access a Tailscale network (known as a tailnet). A Go program using tsnet
can connect to your tailnet as though it were a separate computer. You can also use tsnet
to run multiple services with different confidentiality levels on the same machine. For example, you can create a support tool separate from a data analytics tool, without having to run them on multiple servers or virtual machines. The only way that the tools could be accessed is over Tailscale, so there’s no way to get into them from outside your tailnet.
tsnet.Server
provides the primary means of interacting with a tailnet, and contains all of the state for your program's connection to Tailscale. Use tsnet.Server
to provide your Go program will its own IP address, DNS name, and the ability to grab its own HTTPS certificate. This lets you listen on privileged ports like the HTTP and HTTPS ports without having to run your service as root
. tsnet.Server
builds on the Go standard library HTTP functionality to give you the ability to make HTTP requests and an HTTP server to handle HTTP requests, in the context of your tailnet.
If you use tsnet.Server
without any arguments, it uses default settings until you modify a setting in a subsequent Server
call.
srv := new(tsnet.Server)
if err := srv.Start(); err != nil {
log.Fatalf("can't start tsnet server: %v", err)
}
defer s.Close()
You can change settings for a tsnet.Server
instance up until you run any methods on it. This following sections show which settings you can change.
Server.Hostname
This setting lets you control the host name of your program in your tailnet. By default, this will be the name of your program, such as foo
for a program stored at /usr/local/bin/foo
. You can also override this by setting the Hostname
field:
srv := new(tsnet.Server)
srv.Hostname = "kirito"
Server.Dir
This setting lets you control the directory that the tsnet.Server
stores data in persistently. By default,tsnet
will store data in your user configuration directory based on the name of the binary. Note that this folder must already exist or tsnet
calls will fail.
Here is how to override this to store data in /data/tsnet
:
dir := filepath.Join("/data", "tsnet")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Fatal(err)
}
srv := new(tsnet.Server)
srv.Dir = dir
You can have as many tsnet.Server
instances as you want per OS process, but you will need to change the state directory if this is the case:
baseDir := "/data"
var servers []*tsnet.Server
for _, hostname := range []string{"ichika", "nino", "miku", "yotsuba", "itsuki"} {
os.MkdirAll(filepath.Join(baseDir, hostname), 0700)
srv := &tsnet.Server{
Hostname: hostname,
AuthKey: os.Getenv("TS_AUTHKEY"),
Ephemeral: true,
Dir: filepath.Join(baseDir, hostname),
}
if err := srv.Start(); err != nil {
log.Fatalf("can't start tsnet server: %v", err)
}
servers = append(servers, srv)
}
Server.Ephemeral
This setting lets you control whether the node should be registered as an ephemeral node. Ephemeral nodes are automatically cleaned up after they disconnect from the control plane. This is useful when using tsnet
in serverless environments or when facts and circumstances forbid you from using persistent state.
srv := new(tsnet.Server)
srv.Ephemeral = true
Server.AuthKey
This setting lets you set an auth key so that your program will automatically authenticate with the Tailscale control plane. By default it pulls from the environment variable TS_AUTHKEY
, but you can set your own logic like this:
tsAuthKey := flag.String("ts-authkey", "", "Tailscale authkey")
flag.Parse()
srv := new(tsnet.Server)
srv.AuthKey = *tsAuthKey
This will set the auth key to the value of the flag --ts-authkey
.
Server.Logf
This setting lets you override the logging logic for each tsnet
instance. By default this will output everything to the log.Printf
function, which can get spammy. To disable all tsnet
logs, you can use a value like this:
srv := &tsnet.Server{
Logf: func(string, ...any) { },
}
When you need the logs, for example when debugging, use a command line flag to enable logging:
tsnetVerbose := flag.Bool("tsnet-verbose", false, "if set, verbosely log tsnet information")
hostname := flag.String("tsnet-hostname", "hikari", "hostname to use in the tailnet")
srv := &tsnet.Server{
Hostname: *hostname,
Logf: func(string, ...any) { },
}
if *tsnetVerbose {
srv.Logf = log.New(os.Stderr, fmt.Sprintf("[tsnet:%s] ", *hostname), log.LstdFlags).Printf
}
This will prefix all tsnet
loglines with the prefix [tsnet:<hostname>]
, which can be useful when trying to debug issues.
Server.Start
Once you are done changing your settings, you start the connection to your tailnet using the Start
method:
srv := new(tsnet.Server)
if err := srv.Start(); err != nil {
log.Fatal(err)
}
defer srv.Close()
Most of the other tsnet
calls implicitly call Start
if it has not been called already.
Be sure to close the server instance at some point. It will stay open until either the OS process ends or the server is explicitly closed:
defer srv.Close()
Server.Close
This call releases all resources associated with the tsnet
server. Any calls to the server after this call will fail. All socket connections will be closed. All listeners will be closed. The node will not respond to ping messages. It is the same as turning off Tailscale on a machine in your tailnet.
Create your tsnet
instances in your main
function and then use defer
to clean them up:
srv := new(tsnet.Server)
if err := srv.Start(); err != nil {
log.Fatal(err)
}
defer srv.Close()
Server.Listen
This call creates a network listener in your tailnet. It returns a net.Listener
that returns connections from your tailnet.
srv := new(tsnet.Server)
srv.Hostname = "tadaima"
ln, err := srv.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
}
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hi there! Welcome to the tailnet!")
})))
This method implicitly calls the Start
method if it has not been called already.
Server.ListenTLS
This call creates a TLS-wrapped network listener in your tailnet. It returns a net.Listener
that wraps every incoming connection with TLS using the Let's Encrypt support Tailscale offers. You can use this to create HTTPS services:
srv := new(tsnet.Server)
srv.Hostname = "aegis"
ln, err := srv.ListenTLS("tcp", ":443")
if err != nil {
log.Fatal(err)
}
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hi there! Welcome to the tailnet!")
})))
This method implicitly calls the Start
method if it has not been called already.
Server.ListenFunnel
This call creates a TLS-wrapped network listener that accepts connections from both your tailnet and the public internet using Funnel. You can use this to expose your services to the public internet.
srv := new(tsnet.Server)
srv.Hostname = "ophion"
ln, err := srv.ListenFunnel("tcp", ":443")
if err != nil {
log.Fatal(err)
}
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hi there! Welcome to the tailnet!")
})))
You can create a Funnel-only listener using tsnet.FunnelOnly()
:
publicLn, err := srv.ListenFunnel("tcp", ":443", tsnet.FunnelOnly())
if err != nil {
log.Fatal(err)
}
privateLn, err := srv.ListenTLS("tcp", ":443")
if err != nil {
log.Fatal(err)
}
This lets you have different logic or endpoints exposed within your tailnet and to the general public.
This method implicitly calls the Start
method if it has not been called already.
Server.Dial
This call lets you create outgoing connections to nodes in your tailnet. You can treat the resulting connections like any other network connection in Go.
srv := new(tsnet.Server)
srv.Hostname = "gaga"
conn, err := srv.Dial(r.Context(), "tcp", "yourmachine:80")
if err != nil {
log.Fatal(err)
}
If MagicDNS is enabled, you can use MagicDNS names instead of full DNS names. For example, yourmachine.tail-scale.ts.net
.
This method implicitly calls the Start
method if it has not been called already.
Server.HTTPClient
This is a convenience wrapper that lets you create a HTTP client preconfigured to make outgoing connections in your tailnet.
srv := new(tsnet.Server)
srv.Hostname = "looker"
cli := srv.HTTPClient()
resp, err := cli.Get("http://yourmachine/hello")
if err != nil {
log.Fatal(err)
}
This method does not implicitly call the Start
method if it has not been called already, but when you make any HTTP requests with that client, it will be called at time of use. In the above example, the Start
method will be called when cli.Get("http://yourmachine/hello")
is called.
Server.LocalClient
When you install Tailscale on a computer normally, you can make changes to its configuration using the tailscale
command line tool. tsnet
doesn't offer the ability to use the tailscale
command line tool to change its configuration, but you can use the LocalClient to make all of the same changes. The tailscale
command line tool is built on the back of the LocalClient type.
One common way to use this is to look up identity information for incoming connections in a HTTP server:
s := new(tsnet.Server)
s.Hostname = "aran"
defer s.Close()
ln, err := s.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
lc, err := s.LocalClient()
if err != nil {
log.Fatal(err)
}
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintf(w, "<html><body><h1>Hello, world!</h1>\n")
fmt.Fprintf(w, "<p>You are <b>%s</b> from <b>%s</b> (%s)</p>",
html.EscapeString(who.UserProfile.LoginName),
html.EscapeString(firstLabel(who.Node.ComputedName)),
r.RemoteAddr)
})))
The who
value is of type apitype.WhoIsResponse
. Depending on your jurisdiction's definition of personally-identifying information, this may contain personally-identifying information.
This method implicitly calls the Start
method if it has not been called already.