Document best practice for a site to listen on Tailscale _and_ other interfaces
First off, thanks for this module! It's really helpful for integrating Tailscale into an existing Caddy reverse proxy setup with minimal changes!
One thing that wasn't obvious to me right away was how to make a site available on both normal physical interfaces and via a Tailscale node. (My use case: I have Caddy serving a number of different sites on my LAN. I want to expose a proper subset of those sites to a friend via Tailscale node sharing, without needing to spin up a separate Caddy instance for this.)
Let's say I start with this Caddyfile....
# Only listens on physical interfaces.
site.example.com {
...
}
If I add in Tailscale like so, the site starts listening on the Tailscale node addresses... but it also stops listening on other interfaces. Totally makes sense, as being able to share privately with Tailscale is important. It's just not the use case I specifically have. :)
{
tailscale {
shared {
auth_key ...
hostname shared
state_dir /tailscale
}
}
}
# Only listens on Tailscale node.
# (Useful for some cases, but not what I want here.)
site.example.com {
bind tailscale/shared
...
}
I natively tried also binding on [::], but that just causes Caddy to die with a panic: connection already exists error at startup (presumably since I have other sites without a bind directive that already set up servers on [::]:80 and [::]:443):
# It would be nice if this worked, but it doesn't!
site.example.com {
bind [::] tailscale/shared
...
}
The best solution I could find is defining the site twice and using a snippet to share the implementation details:
(site) {
...
}
# Listens on non-Tailscale interfaces.
site.example.com {
import site
}
# Listens on Tailscale node.
site.example.com {
bind tailscale/shared
import site
}
I'd be happy to send a quick PR adding an example like this to the README, but first I wanted to double check I'm not just being dumb and missing a cleaner way to enable this pattern without the duplicate site + snippet boilerplate.
Yeah, I think having two separate sites is the cleanest way. Happy to review a PR!
# It would be nice if this worked, but it doesn't!
site.example.com {
bind [::] tailscale/shared
...
}
This does work but you must specify the network:
*.example.com {
bind tcp4/0.0.0.0 tailscale/ingress
/// config
}
I had to read through the caddy go code to discover this, but it's the same reason that tailscale/ works - it's just a custom defined network
I haven't tested - but i imagine ipv6 works the same
https://github.com/caddyserver/caddy/blob/master/listeners.go#L638-L654
Interesting. I poked around with that a bit.
site1.example.com {
// no bind here
}
site2.example.com {
bind tcp6/[::] tailscale/node
}
still exits with panic: connection already exists on startup. (It looks like it's setting up two servers internally, both trying to listen on [::]:443.
I can confirm that
site1.example.com {
// no bind here
}
site2.example.com {
bind tcp4/0.0.0.0 tailscale/node
}
does start up successfully, but then site2.example.com isn't listening locally on IPv6.
So unless I'm just missing something obvious, I'll stick with the two-site approach for now. (Which I still need to send a doc PR about, maybe later this weekend.)
Yeah I never tried with tcp6 so something may be off with how the caddyfile adapter is trying to interpret the attribute.
I am (and have been for many weeks) successfully using bind tcp4/0.0.0.0 tailscale/node in my setup - not sure how you're verifying it's not listening on the IPv4 network. I'm running Caddy in docker - but I couldn't imagine that is the difference maker here.
Could be worth trying to scope it even further to see if it could make caddy less confused (e.g. tcp6/[::]:443).
However this could also be an issue with how tsnet is registering the network in caddy - maybe it uses IPv6 internally to bind and that's why IPv4 is working for me.
In any case - two separate sites may be the easiest solution here - but I do agree I wish bind worked as expected here!
Sorry, typo (fixed above). With bind tcp4/0.0.0.0 tailscale/node, it's not listening on IPv6, which I want for my setup. (With the default Debian dual-stack setup, binding on [::] listens on both IPv6 and IPv4, but binding 0.0.0.0 lisetens only on IPv4.)
And yeah, ideally I can get this down to one site, but realistically, two sites with a snippet is fine. I should probably leave that be and move on with my life, but this is as much about messing around and learning as anything, so it's all good. :)