I wanted to run my VPN/Tailscale setup past you, see if anybody has suggestions on how I could do things better.

  • Setup: home LAN (10.0.0.0/24), router+DNS on 10.0.0.1, server running docker containers on 10.0.0.2.
  • LAN DNS points *.local.dom.tld to the server, public DNS points *.dom.tld to my dynamic public IP.
  • Containers run in bridge mode with host, expose ports on host IPs via “ports:” mapping.
  • NPM with LE certs also in container, exposes 10.0.0.2:443, forwards to various other services.

Goals for Tailscale:

  • Accessing HTTP services via NPM from my phone when away from home.
  • Exposing select UDP and TCP non-HTTP services such as syncthing (:22000) or deluge RCP admin (:58846) to other tailnet devices or to phone on the go.

Goals in general:

  • Some containers need to expose ports on the LAN.
  • Some containers need to expose ports via Tailscale.
  • Some containers need to broadcast on the LAN (DLNA stuff) – but I don’t want them broadcasting to Tailscale.
  • Generally speaking I’d like to explicitly control what’s exposed from each container on either LAN or Tailscale.
  • I’d like to avoid hacking images with Dockerfile. I can make my own images to do stuff, just don’t want to keep up with hacking other images.

How I progresed with Tailscale:

  1. First tried running it directly on the host. Good: tailnet IP (let’s call it 100.64.0.2) available on the host’s default network stack. Containers can use “ports:” to map to 100.64.0.2 (tailscale) and/or 10.0.0.2 (LAN). Bad: tailscale would mess with /etc/resolv.conf on host. Also bad: tailscale0 on host picked up stuff that binds to 0.0.0.0.
  2. Moved tailscale to a container running on the host network stack (network_mode: host). Made it leave /etc/resolv.conf alone. tailscale0 on host stack still picks up everything on 0.0.0.0.

This is kinda where I’m stuck. I can make the tailscale container bridged which would put the tailscale0 interface inside the container. It wouldn’t pick up 0.0.0.0 from host but how would I publish ports to it?

  • The tailscale recommended way of doing it is by putting other containers in the tailscale’s container network stack (network_mode: container:tailscale). This would prevent said containers from using “ports:” to map to host anymore. Also, everything they publish locally would end up on tailscale0 whether I like it or not.
  • Tailscale has an env var TS_DEST_IP that can mirror another IP. I could allocate an IP on host eth0 like 10.1.1.1, mirror that from the tailscale container, and target it from other containers explicitly with “ports:” when I want to publish a port to tailscale. Downside: 10.1.1.1 would be in the host’s network stack so still picks up 0.0.0.0.
  • I could bridge the tailscale container with other containers on a private subnet, say 192.168.1.0/24 and use tailscale serve to forward specific ports to other containers over that subnet. Unfortunately serve is fairly limited; it can’t do UDP and technically it refuses to forward TCP either to non-localhost (but you can dump the serve config to JSON, and hack that config, and use it with TS_SERVE_CONFIG= 🤮).
  • I could bridge tailscale with other containers and create a special container with a fixed IP on that subnet, mirror the IP from tailscale, and use iptables on that container to forward specific ports to other containers. This would actually solve everything I want except…
  • If I ever want to use another VPN which doesn’t have the mirror feature. I don’t know how I’d deal with that.
  • @gencha@lemm.ee
    link
    fedilink
    English
    2
    edit-2
    7 months ago

    Sharing the network space with another container is the way to go IMHO. I use podman and just run the main application in one container, and then another VPN-enabling container in the same pod, which is essentially what you’re achieving with with the network_mode: container:foo directive.

    Ideally, exposing ports on the host node is not part of your design, so don’t have any --port directives at all. Your host should allow routing to the hosted containers and, thus, their exposed ports. If you run your workloads in a dedicated network, like 10.0.1.0/24, then those addresses assigned to your containers need to be addressable. Then you just reach all of their exposed ports directly. Ultimately, you then want to control port exposure through services like firewalld, but that can usually be delayed. Just remember that port forwarding is not a security mechanism, it’s a convenience mechanism.

    If you want DLNA, forget about running that workload in a “proper” container. For DLNA, you need the ability to open random UDP ports for communication with consuming devices on the LAN. This will always require host networking.

    Your DLNA-enabled workloads, like Plex, or Jellyfin, need a host networking container. Your services that require internet privacy, like qBittorrent, need their own, dedicated pod, on a dedicated network, with another container that controls their networking plane to redirect communication to the VPN. Ideally, all your manual configuration then ends up with a directive in the Wireguard config like:

    PostUp = ip route add 192.168.1.0/24 via 192.168.19.1 dev eth0
    

    Wireguard will likely, by default, route all traffic through the wg0 device. You just then tell it that the LAN CIDR is reachable through eth0 directly. This enables your communication path to the VPN-secured container after the VPN is up.