tailscale / tailscale/tailscale-rs
Best way to proxy HTTP using the Python library?
@dylan-tailscale is already working on this.
Since Jun 17, 2026.
- Dominant language
- Rust
- Stars
- 1.2k
- Forks
- 61
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 22
Description
I got a prototype Datasette plugin working using tailscale-py:
https://github.com/datasette/datasette-tailscale
It's pretty neat, you can do this with it:
uv tool install datasette>=1.0a32
datasette install datasette-tailscale
datasette tailscale mydata.db --ts-authkey tskey-auth-xxxx --ts-hostname datasette-demo
And now http://datasette-demo/ on your Tailnet should serve that Datasette instance.
But... I mostly vibe-coded it and the solution for serving that localhost port over Tailscale is a little weird:
dev = await tailscale.connect(key_file, authkey, hostname=ts_hostname)
listener = await dev.tcp_listen((ipv4, ts_port))
try:
while True:
stream = await listener.accept()
asyncio.create_task(_proxy(stream, free_port))
finally:
server.should_exit = True
await uvicorn_task
Here's that _proxy() helper (it took some iteration to get things to feel right, hence the Nagle bit):
async def _proxy(ts_stream, port):
"Bridge one tailnet TCP stream to a fresh loopback connection to Datasette."
try:
reader, writer = await asyncio.open_connection("127.0.0.1", port)
except OSError:
return
# Disable Nagle on the loopback socket. Without this, small HTTP writes
# (headers, the tail of a response) can sit unflushed waiting for an ACK,
# which shows up as responses dribbling through or appearing to hang.
sock = writer.get_extra_info("socket")
if sock is not None:
try:
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except OSError:
pass
async def tailnet_to_local():
try:
while True:
data = await ts_stream.recv()
if not data:
break
writer.write(data)
await writer.drain()
except Exception:
pass
# Half-close: tell the backend we're done sending, but keep the socket
# open so it can still write its response back to us. Fully closing here
# would race the response and truncate it.
try:
if writer.can_write_eof():
writer.write_eof()
except Exception:
pass
async def local_to_tailnet():
try:
while True:
data = await reader.read(65536)
if not data:
break
# send() may transmit fewer bytes than offered.
while data:
sent = await ts_stream.send(data)
data = data[sent:]
except Exception:
pass
try:
await asyncio.gather(tailnet_to_local(), local_to_tailnet())
finally:
try:
writer.close()
except Exception:
pass
Is this a good pattern? It feels to me like what I'm doing here - proxying a localhost port via Tailscale - should be a very common use-case, it would be great if the Python library had a more ergonomic solution (or maybe my solution is junk and there's a better way already.)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.