I set net.ipv6.conf.all.forwarding=1 on a Debian gateway so it could pass VPN client traffic, and about half an hour later the box had no IPv6 default route left. The upstream router was up, the link-local neighbor still answered, and the uplink still held its global address. What had stopped was the kernel's willingness to keep taking Router Advertisements on that interface. The route did not vanish when I typed the sysctl; it vanished when the lifetime on the RA-learned default ran out and nothing refreshed it.

Nothing looked broken in the obvious sense. Forwarding worked, clients kept getting through while the old route was still cached, and the failure arrived later than the change that caused it. That delay is why this one gets blamed on the upstream or on a firewall rule that was never involved.

Read the route table before you touch any sysctl

Start with ip -6 route show default and read the attributes, not just the gateway. An RA-learned default is marked proto ra, carries a preference, and shows an expires countdown. A route you added by hand has neither, and that difference tells you which mechanism owns the route.

$ ip -6 route show default
default via fe80::1 dev eth0 proto ra metric 1024 expires 1432sec hoplimit 64 pref medium

Then look at the address: ip -6 addr show dev eth0. A global address with preferred and valid lifetimes measured in hours will outlive a default route whose router lifetime is measured in minutes. Address present plus default missing is the signature of an advertisement problem. Address missing as well means the link, the prefix information, or duplicate address detection is the thing to chase first.

Why forwarding turns RA processing off

An RA is an ICMPv6 packet of type 134, sent from a link-local source with hop limit 255. Before the kernel uses it, it consults the interface's accept_ra. The default value is 1, and 1 does not mean yes. It means accept only while this host is not forwarding. Set forwarding=1 and that condition fails, so the advertisement is dropped before any prefix or default router in it is considered.

Three values exist, and only the third survives forwarding:

accept_raforwarding=0forwarding=1
0ignoredignored
1 (default)acceptedignored
2acceptedaccepted

Two other flags decide what actually gets installed. accept_ra_defrtr controls whether the default router in the advertisement becomes a route, and accept_ra_pinfo controls whether prefix information is used for address configuration. Turning off accept_ra_defrtr is the usual way to keep SLAAC addresses on a forwarding host without learning a default. It is a different lever from accept_ra, and it is the one to reach for when you want addresses but not a route. The kernel IP sysctl reference lists all three.

Confirm it with three commands

First, read the live values instead of the file you believe is loaded:

sysctl net.ipv6.conf.eth0.forwarding net.ipv6.conf.all.forwarding \
       net.ipv6.conf.eth0.accept_ra net.ipv6.conf.eth0.accept_ra_defrtr

Second, prove the advertisements still arrive. Filtering on the ICMPv6 type byte is enough, and it avoids reading a wall of neighbor discovery traffic:

tcpdump -ni eth0 'icmp6 and ip6[40] == 134'

If nothing appears over several minutes, accept_ra is not your problem. The upstream stopped advertising, something on the path filters ICMPv6, or that interface is not really on the segment you think it is. Third, solicit an advertisement yourself and read its fields:

rdisc6 eth0

rdisc6 ships in the ndisc6 package on Debian and Ubuntu. In its output, look at the router lifetime and the preference before you change anything else. An advertisement with a router lifetime of 0 is an instruction to remove the default router, and no accept_ra value turns that into a usable route. This is also how you catch an upstream CPE in bridge mode that hands out prefixes but never offers itself as a router.

Make the uplink the only permissive interface

If the upstream is supposed to be the default router, accept advertisements there despite forwarding, and nowhere else:

# /etc/sysctl.d/60-ipv6-router.conf
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.eth0.accept_ra = 2
net.ipv6.conf.eth0.accept_ra_defrtr = 1

The scoping matters because IPv6 forwarding is one global switch in Linux, not a per-link one. Write 1 to net.ipv6.conf.wg0.forwarding and then read net.ipv6.conf.all.forwarding: the global value flips and every interface starts forwarding. You cannot be a router on the tunnel and a plain host on the LAN. What you can scope is acceptance. Keep accept_ra=2 on the uplink and leave the tunnel and LAN interfaces at 1 or 0, so a stray advertisement arriving over the tunnel cannot install a default that points into it.

A transient sysctl -w will not survive the next reboot, and both systemd-networkd and NetworkManager write these values from their own configuration. In networkd, set IPv6Forwarding=yes and IPv6AcceptRA=yes in the uplink .network file; networkd documents IPv6Forwarding as global for the same kernel reason. With NetworkManager, ipv6.method=auto on the uplink is what drives the accept_ra value, so re-read the sysctl after nmcli connection up rather than trusting the first check.

The static alternative, ip -6 route add default via fe80::1 dev eth0, does work, and it is a reasonable answer when the upstream router has a stable link-local address. Be clear about what you give up. A static default is never refreshed and never withdrawn, so it keeps pointing at a dead neighbor after the upstream is replaced. The RA route fails safe; the static one fails quietly.

Test a forwarded packet, not the gateway's own

The gateway's own pings can succeed on a route no client can use, and they can also succeed because the gateway is the source. Test from a connected client, then ask the gateway what it would do for that client's address:

# on the VPN client
ping -6 -c 3 2001:4860:4860::8888

# on the gateway, using a client address
ip -6 route get 2001:4860:4860::8888 from fd42:20::2

Then wait for one router lifetime and look again. The steady state you want is a default marked proto ra whose expires value keeps resetting, plus all.forwarding=1. If expires only counts down, advertisements are still being dropped somewhere, and the usual culprit is a network manager rewriting accept_ra after the boot-time sysctl has already run.

Before you close this out, confirm four things: the default route is proto ra and refreshing, accept_ra=2 applies only to the uplink, forwarding is still 1 after a reboot and after a manager restart, and a client rather than the gateway completes an IPv6 ping. If the client still fails with a healthy route, the next suspect is the return path or an upstream firewall, not the advertisement.

Qillqakuna t'inkisqa