A Linux VPS can show a scope-global IPv6 address and still have no usable IPv6 path. The address belongs to one interface; egress additionally needs a matching route, a reachable first-hop router, working ICMPv6, an allowed transport and an application that actually completes the request.
That distinction changes the repair. Replacing an address cannot fix a missing default route, and adding a random gateway cannot fix failed Neighbor Discovery. Likewise, a successful ping does not prove HTTPS, DNS or inbound service reachability. Read the layers in order, preserve the first failing receipt and change only the owner of that layer.
This guide is for an operator who can use ip, ping and curl on a Linux VPS. The reproduced lab creates three private network namespaces joined by two veth pairs. It changes no host route, firewall rule, persistent network file or public address, and its six inputs remove every owned namespace and receipt.
Linux’s current ip-address manual calls global a scope value: the address is valid beyond the local link. It does not assert that an upstream provider routes the prefix, that the correct source will be selected or that the gateway answers. RFC 4291 similarly defines an IPv6 address as an identifier assigned to an interface, not as an end-to-end connectivity certificate.
A link-local address beginning with fe80:: is also normal. Every IPv6 interface needs link-local capability for local-link functions, and many providers use a link-local default gateway. Such a gateway must be paired with the correct device because the same link-local text could exist on several interfaces.
Treat the path as five separate questions:
| Layer | Evidence to collect | Acceptance signal | A failed result means | Likely owner |
|---|---|---|---|---|
| Address | intended prefix, scope, lifetime and interface | preferred global address matches the allocation | the guest does not own the expected source identity | image, cloud-init or network manager |
| Route | exact destination, chosen source, next hop and device | lookup names the intended source, gateway and device | the kernel has no usable forwarding decision | persistent route or policy rules |
| Neighbor | gateway state and link-layer binding | first hop has a usable neighbor state | the selected first hop is not answering on that link | gateway, prefix, VLAN, virtual switch or ICMPv6 policy |
| Transport | literal-address ICMPv6 and TCP/HTTPS result | controlled packets and the intended session return | packets or sessions fail beyond name resolution | route, firewall, PMTU or remote listener |
| Application | status, certificate identity and expected response | the returned identity and body match the request | the network works but the service contract does not | DNS, TLS, proxy or application |
Reading in this order prevents false attribution. For example, curl -6 https://example.com can fail before any packet leaves because the name has no AAAA answer. Conversely, a literal-address ping can pass while a web server listens only on IPv4. One green command never substitutes for the missing layers.
Provider IPv6 models differ. A VPS may receive one /128 address plus an off-prefix gateway route, a routed /64, or an address learned through Router Advertisement. Copy the assigned address, prefix length, gateway, interface MAC, expected configuration method and rescue-console instructions from the provider panel before editing the guest.
OVHcloud’s current VPS IPv6 documentation is a useful example of a provider-specific model: its non-persistent recipe installs both a route to the gateway and a default route. EDIS Global’s troubleshooting page likewise tells readers to verify the exact location-specific prefix and gateway. Neither recipe should be copied to a different provider without comparing that provider’s allocation.
Keep an independent return path.
Do not restart Netplan, NetworkManager, systemd-networkd or the whole VPS from the only SSH session that depends on the configuration being replaced. Open and test the provider console or rescue path first. Keep the working IPv4 route until the new IPv6 path has passed both egress and inbound checks.
For Netplan, netplan try can provide a timed rollback when the installed version and renderer support the proposed change. NetworkManager offers checkpoint-based rollback on supported releases. Those mechanisms reduce lockout risk, but the provider console remains the recovery boundary if the renderer never comes back.
Capture state without printing secrets.
Record UTC time, distribution, kernel, active network renderer, interface name, the intended IPv6 prefix and the exact destination used for testing. Do not publish hostnames, account identifiers, internal addresses or firewall sets from a production receipt.
IPv6 can be disabled globally or per interface, and Router Advertisement acceptance interacts with forwarding mode. Read the relevant sysctls before changing them. A blanket disable_ipv6=0 or accept_ra=2 copied from a forum can create a different routing model from the one the provider supports.
Start with ip -6 addr show dev INTERFACE scope global. Reject a tentative address, a dadfailed flag, an expired valid lifetime or a prefix that differs from the provider record. A healthy-looking line proves only the address layer.
Next, query a real intended destination with ip -6 route get DESTINATION. The ip-route manual describes this as a lookup through the kernel routing tables. Its result names the selected route, source, next hop and device; that evidence is stronger than scanning for any line beginning with default.
Multiple source addresses, policy rules, VRFs or containers can select different paths. When the application binds a source, repeat the lookup with from SOURCE. If rules are involved, inspect ip -6 rule show and the table named by the matching rule. Voxfor’s Linux policy-routing test covers the separate rule-priority and source-path problem.
A default route may legitimately point to a link-local gateway, such as default via fe80::1 dev eth0. Another provider may require an explicit on-link route to a gateway outside the address prefix. Never “correct” either shape to a generic /64 merely because a blog uses one.
Once the exact route names a next hop, generate one controlled packet and inspect ip -6 neigh show GATEWAY dev INTERFACE. Linux neighbor states are defined in the ip-neighbour manual. REACHABLE, STALE, DELAY or PROBE can all describe a usable binding at different moments; INCOMPLETE and then FAILED mean address resolution did not obtain a usable link-layer answer.
IPv6 Neighbor Discovery uses ICMPv6 rather than IPv4 ARP. RFC 4861 defines router discovery, address resolution and Neighbor Unreachability Detection. A firewall policy that drops all ICMPv6 can therefore break the protocol itself, not merely make ping less convenient. Keep Neighbor Solicitation, Neighbor Advertisement and Packet Too Big handling within the provider’s documented security policy.
Failed neighbor resolution narrows the incident but does not automatically prove a provider outage. Confirm the device, gateway spelling, prefix/on-link route, virtual NIC state and local firewall. Then send the provider a timestamped address, route-get and neighbor receipt instead of “IPv6 does not work.”
On a production VPS, begin egress testing with a literal IPv6 destination you trust. That removes forward DNS from the first test. Then run curl -6 against the intended hostname and record the actual remote address, status and certificate identity. Voxfor’s guide to curl connection and request identity explains why connection destination, URL authority, TLS name and HTTP Host must remain separate.
If a literal address works but a hostname fails, inspect the AAAA answer, resolver path and application family selection. A broken DNSSEC chain belongs to a different diagnostic path; do not edit the VPS route to repair resolver SERVFAIL. Similarly, normal dual-stack success can hide one failed family because Happy Eyeballs selects the faster path.
Test IPv4 and IPv6 independently when the service promises both. The strict HTTP/3 path test demonstrates the same principle for QUIC: a fallback-capable client can look healthy while one protocol or address family remains broken.
Inbound reachability is another contract. Before publishing an AAAA record, confirm the daemon listens on the intended IPv6 address, the host and provider firewalls admit the port, the certificate covers the hostname and an external IPv6 vantage point receives the expected response. Outbound curl from the VPS cannot prove that clients can enter.
When small packets pass but large transfers stall, preserve route, MTU and Packet Too Big evidence. IPv6 routers do not fragment forwarded packets for the sender. Voxfor’s WireGuard PMTU diagnosis shows how a size boundary differs from total route failure.
scope global mean the IPv6 address is internet-routable?No. scope global is a Linux address-scope property. It does not prove a provider route, default gateway, successful Neighbor Discovery, firewall admission or remote return path. Require an exact route lookup and a real transport result.
Not by itself. Many providers use a link-local router address. The route must name the correct interface, and that gateway must resolve to a usable neighbor state. Follow the provider’s documented model rather than replacing it with a global gateway from another network.
Possible owners include an unreachable gateway, blocked ICMPv6, a wrong prefix or device, an absent upstream route, or a destination without a return path. Inspect ip -6 route get and the gateway neighbor entry before changing DNS or applications.
A blanket ICMPv6 block is unsafe. IPv6 relies on ICMPv6 for Neighbor Discovery and Path MTU Discovery. Apply a type-aware policy appropriate to the host and provider; dropping every type can break address resolution or cause large-packet stalls even when TCP ports are allowed.
curl -6 prove the website is reachable over IPv6?Outbound curl -6 proves only one client path. Inbound service proof needs a listener on IPv6, matching firewall policy, correct AAAA data, valid TLS identity and a request from an external IPv6-capable vantage point.
Send a UTC timestamp, assigned address and prefix, interface, exact ip -6 route get result, gateway neighbor state, literal-address ping or TCP result, hostname result and confirmation that the host’s persistent configuration matches the provider record. Redact account data and private hostnames. That receipt lets support distinguish guest configuration from the virtual switch or upstream route.
Inside the isolated lab, documentation prefixes from 2001:db8::/32 must never be interpreted as public internet space. The client receives a scope-global address, the router joins two prefixes, and the destination serves one HTTP receipt. All names contain a process-specific suffix so the setup refuses an existing namespace.
Input one creates the marker-owned scope and the three namespaces. It intentionally gives the client no default route. Root is required for namespaces, but the host’s own route and firewall tables are untouched.
set -Eeuo pipefail
export LC_ALL=C
lab_root=$(mktemp -d /tmp/voxfor-ipv6-174.XXXXXX)
marker=$lab_root/.voxfor-owned
receipt_copy=$PWD/vps-ipv6-egress-receipt-174.txt
suffix=$$
client_ns=v6c174$suffix
router_ns=v6r174$suffix
dest_ns=v6d174$suffix
client_if=vc174$suffix
router_client_if=vr174a$suffix
router_dest_if=vr174b$suffix
dest_if=vd174$suffix
server_pid=
cleanup() {
if [[ -n ${server_pid:-} ]] && kill -0 "$server_pid" 2>/dev/null; then
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
fi
if [[ -n ${lab_root:-} && -d $lab_root && -f $marker ]] &&
[[ $(<"$marker") == voxfor-ipv6-174 ]] &&
[[ $lab_root == /tmp/voxfor-ipv6-174.* ]]; then
for ns in "${client_ns:-}" "${router_ns:-}" "${dest_ns:-}"; do
[[ $ns =~ ^v6[crd]174[0-9]+$ ]] && ip netns del "$ns" 2>/dev/null || true
done
find "$lab_root" -depth -mindepth 1 -delete
rmdir "$lab_root"
fi
rm -f -- "$receipt_copy"
}
trap cleanup EXIT
test ! -e "$receipt_copy"
printf '%s\n' voxfor-ipv6-174 > "$marker"
for tool in ip ping curl python3 grep awk sha256sum; do command -v "$tool" >/dev/null; done
printf '%s\n' "$client_ns" "$router_ns" "$dest_ns" > "$lab_root/namespaces"
while IFS= read -r ns; do
[[ $ns =~ ^v6[crd]174[0-9]+$ ]]
test ! -e "/run/netns/$ns"
ip netns add "$ns"
done < "$lab_root/namespaces"
ip link add "$client_if" type veth peer name "$router_client_if"
ip link set "$client_if" netns "$client_ns"
ip link set "$router_client_if" netns "$router_ns"
ip link add "$router_dest_if" type veth peer name "$dest_if"
ip link set "$router_dest_if" netns "$router_ns"
ip link set "$dest_if" netns "$dest_ns"
ip -n "$client_ns" link set lo up
ip -n "$client_ns" link set "$client_if" name eth0 up
ip -n "$client_ns" -6 addr add 2001:db8:174:1::10/64 dev eth0 nodad
ip -n "$router_ns" link set lo up
ip -n "$router_ns" link set "$router_client_if" name edge0 up
ip -n "$router_ns" link set "$router_dest_if" name edge1 up
ip -n "$router_ns" -6 addr add 2001:db8:174:1::1/64 dev edge0 nodad
ip -n "$router_ns" -6 addr add 2001:db8:174:2::1/64 dev edge1 nodad
ip netns exec "$router_ns" sysctl -q -w net.ipv6.conf.all.forwarding=1
ip -n "$dest_ns" link set lo up
ip -n "$dest_ns" link set "$dest_if" name eth0 up
ip -n "$dest_ns" -6 addr add 2001:db8:174:2::20/64 dev eth0 nodad
printf 'kernel=%s iproute2=%s namespaces=3 host_routes_changed=no\n' \
"$(uname -r)" "$(ip -Version | awk '{print $3}')"
ip -n "$client_ns" -6 addr show dev eth0 scope global
nodad is used only to make a disposable namespace deterministic. Never suppress Duplicate Address Detection on a production address merely to make it appear usable.
Next, ask the kernel for a route to the destination. This input requires the expected Network is unreachable control even though the global-scope address remains present.
set -Eeuo pipefail
test "$lab_root" != / && [[ $lab_root == /tmp/voxfor-ipv6-174.* ]]
test -f "$marker" && grep -Fqx 'voxfor-ipv6-174' "$marker"
set +e
ip netns exec "$client_ns" ip -6 route get 2001:db8:174:2::20 \
> "$lab_root/no-route.out" 2>&1
no_route_rc=$?
set -e
cat "$lab_root/no-route.out"
test "$no_route_rc" -ne 0
grep -Fq 'Network is unreachable' "$lab_root/no-route.out"
printf 'address_present=yes default_route_present=no route_get_exit=%s\n' "$no_route_rc"
A deliberately nonexistent on-prefix gateway comes next. The route table now looks populated, yet one packet drives the gateway entry from INCOMPLETE to FAILED. That negative control proves why ip -6 route show default is still insufficient.
set -Eeuo pipefail
test -f "$marker" && grep -Fqx 'voxfor-ipv6-174' "$marker"
ip -n "$client_ns" -6 route add default via 2001:db8:174:1::dead dev eth0
set +e
ip netns exec "$client_ns" ping -n -6 -c 1 -W 1 2001:db8:174:2::20 \
> "$lab_root/wrong-gateway.out" 2>&1
wrong_gateway_rc=$?
set -e
cat "$lab_root/wrong-gateway.out"
test "$wrong_gateway_rc" -ne 0
for attempt in 1 2 3 4 5; do
neighbor_state=$(ip -n "$client_ns" -6 neigh show 2001:db8:174:1::dead dev eth0 || true)
[[ $neighbor_state == *FAILED* ]] && break
sleep 1
done
printf '%s\n' "$neighbor_state" | tee "$lab_root/wrong-neighbor.out"
grep -Eq '2001:db8:174:1::dead .*FAILED[[:space:]]*$' "$lab_root/wrong-neighbor.out"
printf 'default_route_present=yes wrong_gateway_neighbor=FAILED egress=no\n'
ip -n "$client_ns" -6 route del default via 2001:db8:174:1::dead dev eth0
Now install the intended gateway on the client and a return route on the destination. route get must name the correct source and first hop; two ICMPv6 replies must return; and the gateway neighbor must reach a usable state.
set -Eeuo pipefail
test -f "$marker" && grep -Fqx 'voxfor-ipv6-174' "$marker"
ip -n "$client_ns" -6 route add default via 2001:db8:174:1::1 dev eth0
ip -n "$dest_ns" -6 route add default via 2001:db8:174:2::1 dev eth0
ip netns exec "$client_ns" ip -6 route get 2001:db8:174:2::20 |
tee "$lab_root/route-get.out"
grep -Eq '^2001:db8:174:2::20 .*via 2001:db8:174:1::1 dev eth0 src 2001:db8:174:1::10' \
"$lab_root/route-get.out"
ip netns exec "$client_ns" ping -n -6 -c 2 -W 1 2001:db8:174:2::20 |
tee "$lab_root/ping.out"
grep -Fq '2 packets transmitted, 2 received, 0% packet loss' "$lab_root/ping.out"
ip -n "$client_ns" -6 neigh show 2001:db8:174:1::1 dev eth0 |
tee "$lab_root/gateway-neighbor.out"
grep -Eq '2001:db8:174:1::1 .* (REACHABLE|STALE|DELAY|PROBE)[[:space:]]*$' \
"$lab_root/gateway-neighbor.out"
ICMPv6 success proves forwarding for that packet shape. The next input starts a literal-address HTTP server, fetches its marker from the client and writes one stable receipt. Brackets identify the IPv6 literal and --noproxy '*' prevents an ambient proxy from owning the test.
set -Eeuo pipefail
test -f "$marker" && grep -Fqx 'voxfor-ipv6-174' "$marker"
mkdir -p "$lab_root/www"
printf 'vps-ipv6-application-egress=ok\n' > "$lab_root/www/receipt.txt"
ip netns exec "$dest_ns" python3 -m http.server 18474 \
--bind 2001:db8:174:2::20 --directory "$lab_root/www" \
> "$lab_root/http.log" 2>&1 &
server_pid=$!
printf '%s\n' "$server_pid" > "$lab_root/server.pid"
for attempt in 1 2 3 4 5; do
if ip netns exec "$client_ns" curl --noproxy '*' -g -fsS \
--connect-timeout 2 "http://[2001:db8:174:2::20]:18474/receipt.txt" \
> "$lab_root/http.out" 2> "$lab_root/http.err"; then break; fi
sleep 1
done
cat "$lab_root/http.out"
grep -Fqx 'vps-ipv6-application-egress=ok' "$lab_root/http.out"
kill "$server_pid"
wait "$server_pid" 2>/dev/null || true
server_pid=
{
printf 'address=2001:db8:174:1::10/64,scope:global\n'
printf 'no_route=reject,route_get_exit:%s\n' "$no_route_rc"
printf 'wrong_gateway=reject,neighbor:FAILED\n'
printf 'correct_route=via:2001:db8:174:1::1,source:2001:db8:174:1::10\n'
printf 'icmpv6=2/2_received\n'
printf 'http=vps-ipv6-application-egress:ok\n'
printf 'host_routes_changed=no\n'
} | tee "$lab_root/receipt.txt" "$receipt_copy"
sha256sum "$receipt_copy"
grep -Fqx 'http=vps-ipv6-application-egress:ok' "$receipt_copy"
Cleanup checks ownership before deleting each namespace. It also removes the portable lab receipt so the exact sequence leaves no residue.
set -Eeuo pipefail
test "$lab_root" != / && [[ $lab_root == /tmp/voxfor-ipv6-174.* ]]
test -f "$marker" && grep -Fqx 'voxfor-ipv6-174' "$marker"
while IFS= read -r ns; do
[[ $ns =~ ^v6[crd]174[0-9]+$ ]]
test -e "/run/netns/$ns"
ip netns del "$ns"
done < "$lab_root/namespaces"
find "$lab_root" -depth -mindepth 1 -delete
rmdir "$lab_root"
trap - EXIT
test ! -e "$lab_root"
while IFS= read -r ns; do test ! -e "/run/netns/$ns"; done \
< <(printf '%s\n' "$client_ns" "$router_ns" "$dest_ns")
rm -f -- "$receipt_copy"
test ! -e "$receipt_copy"
printf 'cleanup=namespaces:3,receipt:1 absent=yes\n'
Representative output from Debian 13, Linux 6.12.96+deb13-amd64 and iproute2 6.15.0:
address=2001:db8:174:1::10/64,scope:global
no_route=reject,route_get_exit:2
wrong_gateway=reject,neighbor:FAILED
correct_route=via:2001:db8:174:1::1,source:2001:db8:174:1::10
icmpv6=2/2_received
http=vps-ipv6-application-egress:ok
host_routes_changed=no
cleanup=namespaces:3,receipt:1 absent=yes
The reproduced path is accepted only when the client owns the intended global-scope address, the no-route and wrong-gateway controls fail for their expected reasons, the corrected route names the intended source and gateway, the gateway has a usable neighbor state, both ICMPv6 probes return, the literal-address HTTP response matches exactly, and all three owned namespaces plus the receipt are absent after cleanup.
For production, the receipt should contain the UTC window, provider allocation, interface, ip -6 route get result for the real destination, gateway neighbor state, literal ICMPv6 result, literal TCP or HTTPS result, hostname result, external inbound result where applicable and the one persistent file or connection profile changed. Repeat the check from the real workload context if a container, VRF or service user has a different network namespace.
Route quality varies by direction and time. After restoring basic connectivity, collect repeated evidence instead of treating one low-latency ping as capacity proof. The VPS network test across route, direction and time supplies the broader performance method.
Persistence requires a second check after a controlled renderer reload and, when justified, an observed reboot. The systemd-networkd wait-online investigation helps when the address and route eventually appear but dependent services start against the wrong readiness contract.
If the proposed IPv6 change removes the working route, leaves the gateway INCOMPLETE or FAILED, changes the selected source unexpectedly, breaks the literal application probe or threatens the only administrative session, stop before reboot. Preserve the failed receipt, use the tested provider console or surviving IPv4 path, restore only the previous network file or connection profile with its original permissions, apply the renderer’s validated rollback method, and rerun the address-to-application sequence. Do not flush all routes, disable the firewall, widen a /128 to /64 or delete unrelated cloud-init state to force a green result.