Reverse proxy of Jellfyin to VLAN matching client IP

1. The problem I’m having:

My home networking setup has an IoT VLAN (100) [10.10.10.0/24] as well as a Home LAN [192.168.1.0/24]. I have a single Jellyfin instance running inside a Proxmox LXC that has two interfaces with an IP on each of the above subnets (and correctly configured matching VLAN tag for IoT).

The goal is to have the single address https://jellyfin.example.com configured within my Google Chromecast, but when the Chromecast switches WiFi connection between the Home WiFi network and the IoT network it will continue to work without additional configuration.

Since I do not have a layer 3 switch, the most important aspect of this setup is that when the Chromecast is on the Home network, it does not attempt to stream from the IoT network (or vice versa), since this will force packets across my 100 MbE router and result in choppy video.

The ideal setup would be to just keep Chromecast on the IoT network always but for various reasons this is not possible.

My criteria for success:

  • Have a single instance of Caddy reverse proxy the Jellyfin service via the correct interface.
    • I believe the bind directive can accomplish this

So before I embark on this project, I am looking for feedback on whether my approach is feasible, based on the Caddyfile configuration pasted below.

Questions I have:

  • Does the bind directive attach the matcher to the interface? It’s hard to tell the cause/effect. I believe attempting a connection from the incorrect LAN would just result in no match, and fall through.
  • Give above, would I still need to:
    • Expose two interfaces to Caddy, same as I’ve done for Jellyfin
    • Resolve the address of Caddy using the source IP of the request, which would require some DNSMasq / PiHole configuration
  • How likely is it that Chromecast will cache the IP when switching LANs?

It’s ok for my DNS service to run on the Home LAN, but I believe mismatched LANs when resolving Caddy’s address would not work based on the configuration I’ve provided, or result in choppy video if I relaxed the matcher.

2. Error messages and/or full log output:

3. Caddy version:

2.8.4

4. How I installed and ran Caddy:

a. System environment:

b. Command:

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz
xcaddy build --with github.com/caddy-dns/porkbun
sudo dpkg-divert --divert /usr/bin/caddy.default --rename /usr/bin/caddy
sudo mv ./caddy /usr/bin/caddy.custom
sudo update-alternatives --install /usr/bin/caddy caddy /usr/bin/caddy.default 10
sudo update-alternatives --install /usr/bin/caddy caddy /usr/bin/caddy.custom 50
sudo systemctl restart caddy

c. Service/unit/compose file:

# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.targe

d. My complete Caddy config:

*.example.com, example.com {
  @jellyfin_home {
    host jellyfin.example.com
    client_ip 192.168.1.0/24
  }
  handle @jellyfin_home {
    bind 192.168.1.92
    reverse_proxy http://192.168.1.4:8096
  }

  @jellyfin_iot {
    host jellyfin.example.com
    client_ip 10.10.10.0/24
  }
  handle @jellfyin_iot {
	bind 10.10.10.100
	reverse_proxy http://10.10.10.17:8096
  }
}

5. Links to relevant resources:

This has a lot of networking questions that are honestly outside of my area of expertise. But I’ll help with the Caddy config part as much as I can.

The bind directive is special, it’s not an HTTP handler directive so it cannot go within a handle. You need the bind to be at the top-level of your site block.

I think this means that for now, you can’t use the “wildcard certificates” pattern if you also need to use bind. But I hope it will become possible in the future with autohttps: Implement `auto_https prefer_wildcard` option by francislavoie · Pull Request #6146 · caddyserver/caddy · GitHub

What the bind directive does is clearer if you run caddy adapt --pretty on your config to see the JSON output (adapt before having bind, then again after, then diff the output to compare how it changes). Basically, it modifies the server’s "listen" address from being just something like :443 (only a port, no IP) to having an IP address to bind to. This also means that since the server’s listen address applies to the whole server, if you need the HTTP routes to be different for each listen address, you need to have two different servers so that you can have different configs within those servers. Site addresses don’t map 1:1 to this, it’s syntax sugar, but it would be really hard to unwind this automatically if we had bind inside handler so you need to unwind it yourself by having it in two different sites so that the Caddyfile adapter can properly generate the "servers" in your JSON config.

2 Likes

Thank you, I appreciate the full explanation. My understanding is that the bind directive applies to the service as a whole, literally binding the listen ports to a particular IP, and cannot be used inside a matcher.

Please correct me if I’m wrong but I’m guessing the default bind address is 0.0.0.0 (i.e. listen on all interfaces). This means I could add a secondary interface on the IoT VLAN and Caddy would respond to queries regardless of the origin interface.

In this case it should still be possible to filter by source IP and I would just have to remove the invalid bind directive. However, I’d be fully dependent on the DNS server also correctly resolving the jellyfin.example.com address to the IP on the correct VLAN. I’m actually having trouble finding good documentation on how to do this in PiHole – Caddy is not the roadblock.

Kinda, except 0.0.0.0 would imply IPv4 only, but it also binds to all IPv6 interfaces as well (i.e. [::] if I remember correctly). So the default is technically "" which is all interfaces for all IP versions, in practice.

Cool :+1:

I’m not sure I can help with the rest of it, maybe someone else can instead.

But yeah FWIW it is a common thing to run a DNS server in your LAN to override the domain to point to a LAN IP for devices within your network. But your situation sounds a lot more complicated with more than one private network in play, and I don’t know how to answer that.

2 Likes

Just FYI the simple solution to this was to:

  • remove the bind directive
  • match on host and client_ip as above
  • Have Caddy listen on multiple IPs as discussed
  • Have DNS server return both of Caddy’s IPs
    • All clients I tested will prefer IP from same subnet when given multiple addresses to choose from

It’s not foolproof, rogue client could choose to resolve to address on OTHER subnet.

Also, this can’t be done in Pi-Hole using Local DNS GUI since multiple mappings are not allowed, but Unbound can accommodate this, and it’s probably possible in Pi-Hole as well using --localise-queries

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.