Phones associate with the access point, receive an address, and then reach nothing at all once the tunnel comes up. hostapd is usually healthy in that state, and so is OpenVPN; what breaks is the default route, which either never existed on the bridge or was replaced by the tunnel without leaving a path to the VPN server itself. This runbook puts that route back where it belongs and proves the result from a client rather than from the Pi. Every step is reversible, and nothing here changes the tunnel's configuration until you know which traffic you want inside it.

Inventory the interfaces before you edit anything

The familiar layout puts the wireless radio and the wired port inside br0, starts hostapd against the bridge, and hands out leases from dnsmasq, while an OpenVPN client creates tun0. Three separate requirements then look like one: access point clients need the Pi as their gateway, the Pi needs a route to the internet to reach its server, and the bridge needs a gateway address those clients can use.

Read the current state before changing it, because these four commands separate an interface problem from a routing problem.

ip -br addr
ip route show
ip link show master br0
bridge link show

If br0 carries an address but no default route, clients reach the Pi and nothing beyond it. If the default route points at tun0 while the tunnel is down, the Pi has no working path at all, and the client symptom looks exactly the same. Both cases look identical from a phone, which is why the route table has to be read directly.

Give the bridge a default route that belongs to it

Raspberry Pi OS commonly leaves the bridge without a default route when the bridge came into existence after the interface file was written. The stanza belongs in /etc/dhcpcd.conf, and it should name the upstream router rather than the Pi.

interface br0
static ip_address=192.168.50.2/24
static routers=192.168.50.1
static domain_name_servers=192.168.50.1

In that example 192.168.50.1 is the existing router and 192.168.50.2 is the Pi's bridge address in the same subnet, so substitute your own plan instead of copying the numbers. The bridge members must not also request routers, or the Pi ends up with competing defaults whose winner depends on metric and timing rather than on intent.

After systemctl restart dhcpcd, ip route show default should list br0 once and no duplicate from the wired port. Confirm the lookup rather than the table, since a stale entry can sit beside a winning one: ip -4 route get 203.0.113.10 has to name br0. That address comes from the TEST-NET-3 documentation range, so it cannot collide with a real server while you read this.

Stop the tunnel from hijacking the path to its own server

An OpenVPN client that accepts redirect-gateway takes the default route over and points it into the tunnel. That is right when every access point client should use the VPN, and wrong when only some destinations should. Either way, the Pi's own connection to the server must not travel inside the tunnel it is still building. OpenVPN adds a host route to the remote through the original gateway when it redirects the default, which is exactly the behaviour that keeps the transport alive; if that route is missing, the tunnel collapses as soon as it establishes.

A quick comparison of that transport path before and after connecting is worth the ten seconds. ip -4 route get 203.0.113.10 should name the underlay device in both states, with only the metric shifting. If it names tun0 after the tunnel comes up, the host route was never installed and the next reconnect will fail.

When only selected prefixes should cross the tunnel, use route-nopull and add the routes you want by hand, or push a narrower set from the server. The bridge keeps the default route in that design, and DNS becomes the next decision: dnsmasq forwarding to a home resolver only makes sense for the names that actually live there.

Decide the forwarding and NAT story

Full tunnel needs forwarding enabled on the Pi and a far-side gateway willing to route the access point subnet. Check forwarding before anything else, because a zero makes every route change irrelevant.

sysctl net.ipv4.ip_forward
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -o tun0 -j MASQUERADE

The NAT rule is the shortcut that works when the server will not accept the access point prefix, at the cost of every client appearing as the Pi's tunnel address in far-side logs. Routing the prefix without NAT keeps addresses visible and depends on a return route for 192.168.50.0/24 on the home side.

Write the choice down next to the firewall rules, since a NAT rule that is invisible in the design becomes unexplainable six months later. Keep IPv6 in the same decision; a working IPv4 path beside a broken IPv6 path still reads as half-broken in a browser.

Verify from a client, then roll back cleanly

Test in this order from a phone or laptop on the access point: a lease from dnsmasq, a ping to the bridge address, then a destination on the far side of the tunnel. Each step isolates one layer instead of producing a single unhelpful failure.

ip -4 route get 203.0.113.10
ping -c 3 -I br0 203.0.113.10

Then confirm the Pi still has its own path out: ip route get 203.0.113.10 should name the underlay device, and ping -c 3 -I br0 203.0.113.10 should answer if the server permits ICMP. If the first two client steps pass and the third fails, the fault sits in forwarding or NAT. If the second step fails, the bridge or DHCP is still incomplete.

Rollback is one file edit and two commands: remove the stanza, restart dhcpcd, and replace the default route explicitly with ip route replace default via 192.168.50.1 dev br0. Do that before the maintenance window closes rather than after, because a Pi that has lost its default route is unreachable from the network you would use to repair it.

Pennadoù liammet