Get started - it's free!
Login
© 2025

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.

Last updated Feb 7, 2025