You have two uplinks, and ip -6 route show prints the same next hop on both of them: default via fe80::1 dev uplink0 and default via fe80::1 dev uplink1. That is not a duplicate-address conflict, and copying the bare next hop fe80::1 into a policy table does not tell the kernel which link you meant. The real question is narrower than the symptom suggests: does the second table need one link or both, and what keeps those routes alive after a reboot. Three conditions, checked in order, settle most of it.

The same fe80::1 is two different neighbors

Link-local addresses are scoped to a single link, so fe80::1 is not one address printed twice; it is two neighbors that happen to share a suffix. The zone index is how a host says which one it means, so fe80::1%uplink0 and fe80::1%uplink1 are different destinations to the kernel, and that suffix never travels to a remote router. RFC 4007 describes the zone model behind this, and it is why a policy table has to carry a device with every link-local next hop.

A VPN configuration generator that copies a numeric zone ID from one host into a template is treating local context as if it were part of the address, and the file it writes is wrong on every machine whose interface names or indices differ. Nor does a policy table merge two links into one: a routed VPN passes packets between networks, but it does not carry a remote Ethernet neighbor's link-local identity across the tunnel, so link-local traffic stays on its own link.

Remove the device and the route becomes unwritable. ip -6 route add default via fe80::1 table 200 cannot resolve the gateway, and iproute2 rejects it with Error: Nexthop has invalid gateway. Add dev uplink0 and the command succeeds, because the device is what pins the next hop to one of those two neighbors. What the device cannot tell you is whether uplink0 is the right link for this table, and a route copied from another host brings that host's interface names with it, which is where the real mistakes come from.

Condition one: one link or two in the table

Start with what the second uplink is for. If both links carry the same traffic and one is a standby, the alternate table should hold both gateways with distinct metrics: same destinations, one preferred path, one obvious second choice. If the uplinks exist because different traffic must leave through different providers, or because this is a split tunnel where some traffic bypasses the VPN while the rest stays inside, the two links do not belong in one table at all. They belong in two tables, chosen by a rule, and the rule chooses the table, not the tunnel: bypassing traffic is marked before it is routed, and the mark is what sends it to the second table.

In that second case the rule decides when a table is consulted, and the route decides where the packet goes once it is:

ip -6 rule add fwmark 0x2 table 201
ip -6 route add default via fe80::1 dev uplink1 metric 100 table 201
ip -6 route add 2001:db8:100::/48 dev uplink1 metric 100 table 201
ip -6 route show table 201

Neither object substitutes for the other, and a table holding only a default can still fail to resolve its gateway. If an add is rejected, or the route is present but unusable, put the connected prefix for that interface into the same table, or mark the hop onlink when that prefix is routed elsewhere. In the block above, 2001:db8:100::/48 stands in for that connected prefix on uplink1, not for an arbitrary destination. The fwmark rule only consumes a mark that something else has to set, from an nftables or iptables rule on the forwarding path or from SO_MARK on the socket, so a block copied without a mark source matches nothing.

Condition two: metrics do not detect a dead provider

A metric is a preference among routes that exist, not a health check. When the carrier drops, the kernel removes the routes on that device and the second default takes over. When the cable stays up and the provider behind it dies, nothing in the table changes: the preferred route is installed, the neighbor is reachable at layer 2, and packets leave into a black hole. If that distinction matters for your setup, failover has to come from something that probes the path and removes the route, whether that is a routing daemon or a small script that deletes and reinstalls the entry.

Verify the choice after the table is built, and verify it the way the kernel will see it. ip -6 route get 2001:db8:100::1 prints the route that would be used, including the device and the selected source address, and it honors policy rules. A source address belonging to the wrong uplink is the usual sign that a rule matched earlier than you expected.

Condition three: decide who owns persistence

A route added with ip -6 route add survives until reboot or until something flushes it. A default learned from router advertisements carries a lifetime and can be withdrawn by the router, while a manually added default never expires on its own. Mixing the two in one table is how a stale permanent route ends up outranking a fresh advertised one, long after the uplink it described stopped existing.

Pick one owner per table. If a network manager or routing daemon already manages the interface, describe the policy route there and leave the kernel alone. If you are scripting it, make the script idempotent and write the teardown at the same time, because you will need it during the first bad change:

ip -6 route flush table 201
ip -6 rule del fwmark 0x2 table 201

On a router that forwards for other hosts, check net.ipv6.conf.all.forwarding before blaming the table. With forwarding off, packets addressed to another network are dropped no matter how correct table 201 is.

Record the design so the next person can audit it

An inventory that stores only fe80::1 throws away the fields that matter. Record the destination, the next hop, the device, the table and the metric together, plus where each route came from, whether advertised, manager-generated or static. When someone later asks whether the second uplink is actually carrying traffic, that record answers the question without guesswork.

Before you call the table finished, confirm the neighbor with ip -6 neigh show dev uplink0, re-read ip -6 rule show for ordering accidents, and test with traffic that matches the rule instead of a ping that takes the default route. Then take one uplink down on purpose and watch which route the kernel falls back to; the answer should match the one you wrote down.

ХӀокхунах тера йаззамаш