Bypassing CGNAT for Plex with Tailscale funnels

Status: CURRENT — WITH CAVEAT
Last reviewed: 31 August 2026
Applies to: Tailscale Funnel, Plex Media Server, Proxmox VM/LXC

This method still works and I could not find anything in Tailscale’s current Terms of Service or Acceptable Use Policy that explicitly prohibits Plex or media streaming through Funnel. However, Funnel traffic has non-configurable bandwidth limits, and Tailscale’s AUP prohibits use that creates an undue burden on its service. So yes, it’s still a handy CGNAT workaround — just don’t treat Tailscale like you’ve accidentally discovered a free unlimited Plex CDN and absolutely send it 😅.

This is not legal advice. Terms and product limits can change, so check Tailscale’s current documentation and policies before relying on Funnel for heavy traffic.

I’m currently on a static public IP and run Plex with normal remote access now, so I don’t actually need this workaround anymore. I did spend plenty of time fighting CGNAT before that though, and I remember the pain well enough to keep this guide alive.

With IPv4 addresses becoming scarce, more ISPs are putting customers behind CGNAT. The annoying bit for self-hosting is that you no longer have your own publicly reachable IPv4 address, so normal port forwarding for Plex may not work. Your router can forward the port perfectly and the internet still basically says, “yeah nah”.

Why Tailscale Funnel?

Tailscale Funnel can expose a service running on a Tailscale device to the public internet using a .ts.net HTTPS address. Traffic reaches Tailscale’s Funnel relay servers and is then proxied back to the service on your machine.

For Plex, that means you can expose local port 32400 through a public HTTPS address even if your ISP has you trapped behind CGNAT. That’s the neat bit we’re here for.

A better option when possible: If every device that needs Plex can run Tailscale, don’t use Funnel at all. Install Tailscale on the Plex server and client devices and access Plex privately across your tailnet. No point making something public just for the hell of it. Funnel is mainly useful when you need a normal public HTTPS endpoint — for example, a Plex client that cannot run Tailscale itself.

Tailscale and Cloudflare terms

As of 31 August 2026, I could not find an explicit rule in Tailscale’s Terms of Service or Acceptable Use Policy banning Plex, video streaming or media servers through Funnel.

There are still two important catches:

  • Tailscale documents that Funnel traffic is subject to non-configurable bandwidth limits.
  • Tailscale’s Acceptable Use Policy prohibits use that interferes with, disrupts or creates an undue burden on the Tailscale service or connected infrastructure.

So while personal Plex streaming through Funnel does not appear to be explicitly prohibited, I wouldn’t take the piss and treat it as a free unlimited CDN or relay. Heavy or continuous streaming may hit bandwidth limits and could potentially become an acceptable-use issue.

Cloudflare Tunnel is different. Cloudflare’s current documentation states that public-hostname Tunnel traffic on Free, Pro and Business plans is subject to service-specific terms requiring an appropriate paid service to serve video and other large files. So I would not use a standard public Cloudflare Tunnel as a Plex streaming workaround on those plans.

Set up the Tailscale domain

Create a Tailscale account if you don’t already have one, then open the Tailscale Admin Console and head to DNS.

Tailscale DNS settings showing the tailnet domain

You can hit Rename tailnet until you get something cool 😉

For this example I’ll use:

cool-kid.ts.net


Make sure MagicDNS and HTTPS Certificates are enabled. The Funnel command can also prompt you to enable the requirements if they aren’t already configured.

Tailscale MagicDNS and HTTPS certificate settings

Install Tailscale on the Plex VM or LXC

Install Tailscale using the current Linux installation instructions, or use Tailscale’s install script:

curl -fsSL https://tailscale.com/install.sh | sh


If Plex is running inside an unprivileged Proxmox LXC

Tailscale needs access to /dev/net/tun. Older Proxmox guides manually edited the LXC config with cgroup and bind-mount entries, but current Proxmox versions can do this directly from the web interface. Thankfully, no cgroup archaeology required anymore.

Shut down the LXC, then in Proxmox go to:

LXC → Resources → Add → Device Passthrough

Enter the following Device Path:

/dev/net/tun


Start the LXC again. The equivalent configuration entry is:

dev0: /dev/net/tun


You can confirm the TUN device exists from inside the LXC:

ls -l /dev/net/tun


Tailscale also supports userspace networking if you don’t want to pass through /dev/net/tun, but for a normal Proxmox LXC the native device passthrough method is straightforward and has a lot less weird shit hiding underneath it.

Connect the server to Tailscale

For both a VM and LXC, bring Tailscale up:

tailscale up


Follow the authentication link it gives you. Once complete, the Plex server should appear in your Tailscale Admin Console.

Create the Plex Funnel

Now for the bit we actually came here for. Plex listens on port 32400, so run:

tailscale funnel --bg http://127.0.0.1:32400


The first time you use Funnel, Tailscale may give you a link to approve the required Funnel permissions for the tailnet. One little hoop to jump through, then you’re off.

Once enabled, the output should look similar to:

Available on the internet:

https://plex.cool-kid.ts.net/
|-- proxy http://127.0.0.1:32400

Funnel started and running in the background.


The --bg flag keeps the Funnel configuration running in the background and Tailscale documents that it will resume after a reboot or a tailscale down/tailscale up cycle.

You can check the current Funnel configuration at any time with:

tailscale funnel status


To remove the Funnel later:

tailscale funnel --https=443 off


Tell Plex about the Funnel address

Open your local Plex server:

http://<YourIP>:32400/web


Go to Settings → Server → Network, enable Show Advanced, then find Custom server access URLs.

Enter the HTTPS address created by Funnel. I explicitly include port 443 so Plex publishes the correct external HTTPS endpoint rather than inheriting the normal Plex remote-access port:

https://plex.cool-kid.ts.net:443


Save the changes and restart Plex for good measure. Probably not strictly necessary every time, but Plex has taught me not to argue with a cheap reboot 🙂

About Plex Remote Access: You do not need a normal router port-forward for this Funnel method. If native Plex Remote Access cannot work because you’re behind CGNAT, the custom Funnel URL gives Plex another address to advertise. I no longer recommend explicitly disabling Plex Remote Access as a required step: leaving it enabled can preserve Plex Relay as a fallback if direct access fails. Basically, don’t fight Plex more than you need to — the Funnel URL is the important part of this setup.

Plex custom server access URL configured with Tailscale Funnel

Test it

Test from a device that is genuinely outside your home network — mobile data is an easy option. Don’t leave your phone on Wi-Fi, see Plex working and congratulate yourself too early 😅. Open Plex through the normal Plex app or hosted web app and start a stream.

If it isn’t working, check the chain in order before changing six things at once and creating an entirely new problem:

  1. tailscale status — confirm the Plex server is connected to your tailnet.
  2. tailscale funnel status — confirm the Funnel is active.
  3. Open the https://...ts.net Funnel address directly in a browser and confirm it reaches Plex.
  4. Check Plex Settings → Server → Network → Custom server access URLs contains the correct Funnel URL with :443.

One quirk I found with this method is that remote connections may not appear in the Plex dashboard graph the same way a normal direct remote connection does. Plex being Plex — if the stream works, don’t let one slightly weird graph send you down a three-hour rabbit hole.

Keep the rest of your homelab private

If you also use Tailscale for private access to the rest of your self-hosted services, you don’t need to Funnel everything publicly. Expose the thing that needs exposing and leave the rest of the homelab tucked away where random internet goblins can’t poke it.

Tailscale’s Split DNS can send requests for your own domain — for example richay.au — to your Pi-hole or AdGuard DNS server while you’re connected to the tailnet. Your internal DNS can then point those names at your private reverse proxy.

That gives you public access only where you deliberately want it, while the rest of the homelab stays private. My guide is here: Tailscale Split DNS by Domain for Secure Home Server Access.

For normal public websites like this blog, Cloudflare Tunnel is still a great CGNAT workaround. The video/large-file restriction discussed above applies to public-hostname traffic serving those types of content; it doesn’t mean Cloudflare Tunnel itself is unsuitable for ordinary websites.

Archived notes from the original guide

ARCHIVED — older Proxmox LXC method

The original version of this article manually added:

lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file

Those lines are still relevant to older Proxmox releases, but current Proxmox versions support /dev/net/tun through native LXC Device Passthrough and Tailscale’s own current Proxmox/LXC documentation recommends the GUI method.

References

And that’s it. CGNAT can remain cactus, Plex can still get out, and you haven’t had to beg your ISP for a public IPv4 address. Not a bad little workaround 🙂

Tailscale split dns by domain for secure home server access

Status: CURRENT
Last reviewed: 31 August 2026
Applies to: Tailscale Split DNS, subnet routers, AdGuard Home and a private reverse proxy

This setup lets you keep selected services publicly reachable while resolving your private services to internal IP addresses only when you’re at home or connected to Tailscale. Basically: public where I actually want public, private everywhere else. Much nicer than throwing the whole bloody homelab onto the internet.

What this setup does

I use the same domain for both public and private services. Public services can still be reachable normally, while private subdomains resolve through my own DNS server and point at my internal reverse proxy when I’m connected to Tailscale. One domain, two completely different paths depending on where I am. Neat little trick 🙂

For this example:

  • Domain: richay.au
  • AdGuard Home: 10.10.10.10 — local DNS / ad blocker
  • Traefik v3: 10.10.10.20 — internal reverse proxy
  • AdGuard DNS rewrite: *.richay.au → 10.10.10.20
  • Cloudflare Tunnel: only for services I deliberately expose publicly, such as this blog
  • Tailscale: remote private access, subnet routing and Split DNS

The important distinction is that DNS and routing are separate things. AdGuard can correctly answer private.richay.au → 10.10.10.20, but your remote device still needs a Tailscale route to 10.10.10.20 or the browser won’t be able to reach it. DNS can tell you exactly where the house is; it doesn’t magically build the bloody road there.

Split DNS is not an access-control system. Split DNS decides which DNS server answers a query. Subnet routes decide how packets reach the private IP. Tailscale Grants/ACLs decide whether that traffic is actually allowed. Three different jobs, even though they’re all holding hands by the end of this guide.

1. Install Tailscale on the subnet router

Install Tailscale on a machine that can reach both your DNS server and reverse proxy over the LAN. It doesn’t need to be anything fancy — it just needs to sit in the right spot and not randomly disappear when you need it.

In this example I’m installing Tailscale on the Traefik host at 10.10.10.20. That machine will also act as a Tailscale subnet router.

You can install Tailscale using the current Linux installation instructions.

2. Enable IP forwarding

Tailscale requires IP forwarding on a Linux subnet router. For most modern Linux distributions with /etc/sysctl.d:

echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf


If you’re only using IPv4 you technically only need IPv4 forwarding, but I’ve left Tailscale’s current IPv4 + IPv6 example intact. Future me can deal with IPv6 properly one day instead of pretending it doesn’t exist 😅.

3. Advertise the private routes

This is the bit I had slightly wrong in the original version of this guide. DNS was working, so naturally I stared at DNS for too long even though the missing bit was actually routing. Classic.

The remote Tailscale client needs to reach both the DNS server at 10.10.10.10 and the reverse proxy at 10.10.10.20. Advertising only the AdGuard address lets DNS queries work, but doesn’t create a route to the private IP returned by AdGuard. So you get a perfectly correct DNS answer followed immediately by absolutely fuck-all happening in the browser.

For the tightest routing, advertise only those two hosts:

sudo tailscale set --advertise-routes=10.10.10.10/32,10.10.10.20/32


If you deliberately want your Tailscale devices to access the whole LAN instead, you could advertise the subnet:

sudo tailscale set --advertise-routes=10.10.10.0/24


I prefer the two /32 routes for this guide because they expose only the DNS and reverse-proxy hosts rather than the whole subnet. No need to hand Tailscale the keys to the whole neighbourhood when two houses will do.

4. Approve the routes in Tailscale

Open the Tailscale Machines page, locate the subnet-router machine, open its menu and choose Edit route settings.

Approve 10.10.10.10/32 for AdGuard and 10.10.10.20/32 for Traefik.

Route approval and Tailscale access policy are separate. The route makes those addresses reachable through the tailnet; your Grants/ACLs decide who is actually allowed to connect to them. Reachable does not automatically mean invited in.

Linux client note: Windows, macOS, iOS and Android accept advertised subnet routes by default. Linux clients do not. On a Linux client that needs to use these routes, run:

sudo tailscale set --accept-routes=true


5. Configure Tailscale Split DNS

Now open the Tailscale DNS page.

  1. Select Add nameserver → Custom.
  2. Enter your internal DNS server: 10.10.10.10.
  3. Enable Restrict to search domain.
  4. Enter richay.au as the search domain.
  5. Save the nameserver.

Tailscale calls this a restricted nameserver. It is also commonly called Split DNS. Same idea, slightly more corporate name.

With that configured, DNS queries for names under richay.au are sent to AdGuard while you’re connected to Tailscale. Normal DNS queries can continue using the device’s normal resolver.

You do not need to enable Override DNS servers just to use this Split DNS setup. Override DNS servers is for forcing global tailnet DNS behaviour, whereas here we only want richay.au queries sent to AdGuard. No point hijacking every DNS query when we’re only interested in our own little corner of the internet.

Tailscale Split DNS custom nameserver configuration

6. Configure AdGuard Home

AdGuard now needs to return the internal Traefik address for your private services. This is where the whole thing starts feeling suspiciously clever for something that’s actually pretty simple.

In my setup I use a wildcard DNS rewrite equivalent to:

*.richay.au  →  10.10.10.20


This means names such as:

code-server.richay.au
proxmox.richay.au
homeassistant.richay.au


can all resolve to Traefik’s private IP without creating public DNS records for every private service. Which is exactly what I want — nice names internally, no giant public sign saying “hey look, here’s all my homelab shit”.

The original article called this a CNAME record. That’s not quite right when you’re directly rewriting a wildcard name to an IP address — this is better described as an AdGuard wildcard DNS rewrite. Tiny terminology fix, but if I’m going back through these articles I may as well stop lying to future me 😄.

7. Let Traefik route the hostname

Traefik can continue routing each hostname to the correct application exactly as it does on your LAN. It doesn’t care that the request came in through Tailscale; as far as Traefik is concerned, business as usual.

The connection path is:

Remote device
    ↓
Tailscale
    ↓
Split DNS query for app.richay.au
    ↓
AdGuard 10.10.10.10
    ↓
Returns 10.10.10.20
    ↓
Tailscale subnet route
    ↓
Traefik 10.10.10.20
    ↓
Private web app


Public applications can remain completely separate. For example, this blog can still have a public Cloudflare Tunnel while code-server.richay.au exists only in your private DNS. Public stuff stays public, private stuff stays private, and the two don’t need to become one giant networking casserole.

Access control

Don’t confuse private DNS with permission. Hiding the sign doesn’t lock the door.

A device that knows the private IP does not automatically gain access unless your Tailscale policy allows the connection. Tailscale now recommends Grants for new access-control policies; legacy ACLs still work but aren’t receiving new features.

If your tailnet is just your own trusted devices, the default policy may be enough. For a larger tailnet, use Grants to limit which users/devices can reach the advertised private addresses and ports.

Test the setup

Connect a phone or laptop to Tailscale while away from your home network. Mobile data is handy here so you know you’re genuinely testing the remote path and not accidentally proving that your own Wi-Fi still works. First confirm the private DNS server and reverse proxy are reachable:

ping 10.10.10.10
ping 10.10.10.20


On Windows, Tailscale recommends using Resolve-DnsName rather than relying on nslookup when testing advanced DNS behaviour such as Split DNS:

Resolve-DnsName -Name code-server.richay.au


The result should return 10.10.10.20. Then open the private hostname in your browser:

https://code-server.richay.au


Troubleshooting

If DNS resolution fails, check that Tailscale can reach AdGuard and that the restricted nameserver is configured for the correct domain. Don’t start kicking Traefik yet — it hasn’t even been invited to this part of the problem.

If DNS resolves correctly to 10.10.10.20 but the website won’t open, that’s usually a routing or access-policy problem, not DNS. Confirm the Traefik address is included in the advertised and approved routes. This is the point where DNS has done its job and gets to sit back while routing takes the blame.

Useful checks:

tailscale status
tailscale status --routes


If a Linux client can reach normal Tailscale 100.x addresses but not the advertised LAN addresses, enable subnet route acceptance:

sudo tailscale set --accept-routes=true


If Linux DNS itself is behaving strangely, make sure your resolver configuration is compatible with Tailscale. On distributions that use it, Tailscale recommends systemd-resolved rather than a manually overwritten or locked /etc/resolv.conf.

The end result

With this setup I can keep genuinely public services public, while the rest of my homelab uses the same nice *.richay.au hostnames without exposing those applications to the internet.

At home, AdGuard resolves the private names directly. Away from home, Tailscale Split DNS sends only richay.au lookups back to AdGuard, and the subnet routes carry the connection to Traefik.

Much nicer than remembering a pile of IP addresses and ports 🙂

Once this is working it’s one of those setups that feels almost suspiciously tidy: same domain everywhere, no public DNS records for the private stuff, and remote access behaves like you’re sitting at home. Took a few moving parts to get there, but bloody hell it’s nice when it all clicks 🙂

References