A proxy server that makes it easy to connect a local HTTP server to a Tailscale network. Optimized for containerized usage but can also be used as a command line tool.
tailproxy myhost localhost:3000
This will prompt you to approve a new device on your tailnet called myhost
. Once approved (by clicking the printed link), you can visit http://myhost
in a browser and it will proxy all requests to the the server listening at localhost:3000
.
Use the package ghcr.io/j-f1/tailproxy
in your docker-compose.yml
:
version: '3'
services:
tailproxy:
image: ghcr.io/j-f1/tailproxy:v1
environment:
- TAILPROXY_NAME=myhost
- TAILPROXY_TARGET=server:8080
- TS_AUTHKEY=${TS_AUTHKEY}
volumes:
- ./tailproxy-data:/home/nonroot/data
links:
- server
server:
container_name: server
image: my-server-image
Make sure to set a valid TS_AUTHKEY
environment variable (see below) when running docker compose up
to ensure that the proxy can join without requiring manual approval. While not required, it’s recommended to mount a volume to /home/nonroot/data
so that the proxy can persist its state between restarts, including TLS certificates. (Otherwise, you’ll have to wait for a new certificate to be generated every time you restart the proxy.)
You are required to provide the following options:
- The machine name (env variable:
TAILPROXY_NAME
or first argument to the CLI) to join your tailnet as. Note that Tailscale will automatically add a-<number>
suffix to the name if it’s already taken. - The target to proxy to (env variable:
TAILPROXY_TARGET
or second argument to the CLI). Format it ashost
(to use the default port 80)host:port
orhost:port/basepath?foo=bar
(in which case/basepath
will be prepended to all requests to the upstream server and?foo=bar
will be prepended to the query string of all requests).
Additionally, you can set any environment variables that are supported by Tailscale. You’ll most likely want to set the TS_AUTHKEY
environment variable to a valid auth key so that you don’t have to click the link to approve the new device every time you restart the proxy. Make sure to configure the auth key to provision pre-approved devices when creating it for the smoothest experience.
You may optionally set TAILPROXY_DATA_DIR
to a directory where the proxy can store its state. Currently, we’re just storing the Tailscale state (which is placed in the tailscale
subdirectory of the directory you provide). If you don’t set this, Tailscale will use /data
if it exists, or a subdirectory named tsnet-tailproxy
in Go’s os.UserConfigDir
if /data
does not exist.
If you write your target as tcp://host:port
, tailproxy will proxy TCP connections to the specified host and port. This is useful if you want to proxy a non-HTTP server, such as a database. The port is required since there isn’t a default port for TCP connections.
Note that HTTPS/TLS and therefore Funnel are not supported because they require TLS termination, which has not been implemented for raw TCP connections. (PRs welcome! You would probably need to rethink the https
option to make it work for raw TLS as well as HTTPS.)
You can optionally pass an option to enable HTTPS support (--https
in the CLI or TAILPROXY_HTTPS_MODE
as an environment variable). The following values are allowed:
off
(default): No HTTPS support. The proxy will only listen on port 80.redirect
: The proxy will listen on both port 80 and port 443. Any HTTP request will be redirected to HTTPS.only
: The proxy will listen for HTTPS requests on port 443. HTTP requests will not be accepted.both
: The proxy will listen for HTTP and HTTPS requests on ports 80 and 443 respectively. It will not redirect HTTP requests to HTTPS.
If HTTPS is enabled, tailproxy will use Tailscale’s API to generate a valid certificate for <host>.<tailnet name>.ts.net
. It will strip HTTPS and forward the plain HTTP request to the upstream server.
Warning: Tailproxy is relatively safe because it’s only accessible from devices you control. However, Funnel allows anyone from the Internet to talk to your server. That means you have to worry about both the security of Tailproxy and of your server. I don’t know about you, but I don’t really know what I’m doing, so besides the inherent safety of Go and the relative simplicity of my code I can’t guarantee that there aren’t any security issues. Use Funnel at your own risk.
You can optionally make the service behind tailproxy publicly accessible using Tailscale Funnel (--funnel
in the CLI or TAILPROXY_FUNNEL_MODE
as an environment variable). The following values are allowed:
off
(default): No Funnel support. The proxy will only listen on your tailnet.on
: The proxy will listen on both your tailnet and the public internet.only
: The proxy will listen for requests only on the public internet. Requests on your tailnet will not be accepted.
Note that you’ll have to enable Funnel for your tailnet, and make sure that the tailproxy node has the funnel
attribute in nodeAttrs
. Funnel handles the TLS termination, so the HTTPS config option will be ignored for connections coming from Funnel.
If you pass TAILPROXY_PPROF_ENABLED=1
(or --pprof
), the proxy will expose a pprof server on port 6060 (on your tailnet). You can use this to debug performance issues or to profile the proxy. Note that the pprof server is only available on your tailnet, even if Funnel is enabled.
The proxy will set several headers on the request it sends to the upstream server based on information Tailscale provides about the device making the request:
X-Forwarded-For
: the IP address of the device making the requestX-Forwarded-Host
: the hostname of the proxy (according to the client making the request)X-Forwarded-Proto
: the protocol used to make the request (eitherhttp
orhttps
)X-Tailscale-WhoIs
:ok
if the request came from a Tailscale device. The below headers identify the device and its owner.funnel
if the request came from Funnel. The below headers will not be present, since Funnel does not authenticate the request.error
if the call to Tailscale failed. The below headers will not be present. This is unlikely to happen, and if you rely on authentication, you should probably return an error to the client in this case. We don’t handle this in tailproxy to give you flexibility to continue on if you don’t care about authentication. Check your server logs and report a bug if this happens to you!
X-Tailscale-User
: the unique user ID of the owner of the device making the requestX-Tailscale-User-LoginName
: the login name (j-f1@github
) of the userX-Tailscale-User-DisplayName
the display name (Jed Fox
) of the userX-Tailscale-User-ProfilePicURL
the URL of the user’s profile picture (if available)X-Tailscale-Caps
a comma-separated list of capabilities the user has (if any)X-Tailscale-Node
the unique ID of the device making the requestX-Tailscale-Node-Name
the machine name of the device making the request (with the tailnet name appended if the device has been shared from another tailnet)X-Tailscale-Node-Caps
a comma-separated list of capabilities the device has (if any)X-Tailscale-Node-Tags
a comma-separated list of tags the device has (if any)X-Tailscale-Hostinfo
a JSON object containing some info about the device making the request
Any X-Tailscale-*
headers sent by the client will be stripped before the request is proxied.