Created
July 13, 2026 01:57
-
-
Save c2h2/ae07a4c818457b4cf3f087bf164059f8 to your computer and use it in GitHub Desktop.
Photonicat 2 (OpenWrt): one-shot script to enable IPv6 on a PPPoE WAN — automates https://photonicat.com/wiki/Photoincat_2_IPv6_PPPoE (WAN ipv6=auto, LAN ip6assign=60, hybrid RA/DHCPv6, docker ip6tables fix, verification)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # pcat2_pppoe_ipv6.sh — Enable IPv6 over a PPPoE WAN on Photonicat 2 (OpenWrt) | |
| # | |
| # Automates the manual steps from: | |
| # https://photonicat.com/wiki/Photoincat_2_IPv6_PPPoE | |
| # | |
| # What it does: | |
| # 1. WAN : network.wan.ipv6='auto' (request IPv6 address + delegated prefix | |
| # over the PPPoE session), disables the legacy wan6 interface | |
| # 2. LAN : network.lan.ip6assign='60', removes ip6class (the "ULA-only" trap) | |
| # 3. RA : dhcp.lan.ra='hybrid', dhcp.lan.dhcpv6='hybrid' | |
| # (optional: --ra-high sets RA preference to high for multi-router LANs) | |
| # 4. Docker (26.04+): adds "ip6tables": false to /etc/docker/daemon.json, | |
| # keeping all other keys untouched | |
| # 5. Verifies prefix delegation and IPv6 connectivity | |
| # | |
| # The firewall needs no changes: fw4 already allows LAN->WAN IPv6 forwarding | |
| # and the required ICMPv6 by default. | |
| # | |
| # Usage — run AS ROOT in a terminal ON THE PHOTONICAT 2 (ssh root@192.168.8.1): | |
| # sh pcat2_pppoe_ipv6.sh [--ra-high] [--force] [--no-ping] | |
| # | |
| # --ra-high also set dhcp.lan.ra_preference='high' (multi-router networks) | |
| # --force proceed even if the WAN protocol is not 'pppoe' | |
| # --no-ping skip the final IPv6 connectivity test | |
| # | |
| # Config is backed up to /tmp/pcat2_ipv6_backup_<timestamp>/ before any change. | |
| # NOTE: applying the change briefly restarts the PPPoE session (a few seconds | |
| # of internet downtime). | |
| set -u | |
| # ---------- pretty output ---------------------------------------------------- | |
| if [ -t 1 ]; then | |
| C_G='\033[1;32m'; C_Y='\033[1;33m'; C_R='\033[1;31m'; C_B='\033[1m'; C_0='\033[0m' | |
| else | |
| C_G=''; C_Y=''; C_R=''; C_B=''; C_0='' | |
| fi | |
| ok() { printf "${C_G}[ ok ]${C_0} %s\n" "$1"; } | |
| warn() { printf "${C_Y}[warn]${C_0} %s\n" "$1"; } | |
| fail() { printf "${C_R}[fail]${C_0} %s\n" "$1" >&2; } | |
| info() { printf "${C_B}[....]${C_0} %s\n" "$1"; } | |
| die() { fail "$1"; exit 1; } | |
| usage() { | |
| sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' | |
| } | |
| # ---------- options ---------------------------------------------------------- | |
| RA_HIGH=0; FORCE=0; DO_PING=1 | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --ra-high) RA_HIGH=1 ;; | |
| --force) FORCE=1 ;; | |
| --no-ping) DO_PING=0 ;; | |
| -h|--help) usage; exit 0 ;; | |
| *) die "unknown option: $arg (try --help)" ;; | |
| esac | |
| done | |
| # ---------- sanity checks ---------------------------------------------------- | |
| command -v uci >/dev/null 2>&1 || \ | |
| die "uci not found. Run this ON the Photonicat 2 itself (ssh root@192.168.8.1), not on your PC." | |
| [ "$(id -u)" = "0" ] || die "must run as root" | |
| WAN_PROTO=$(uci -q get network.wan.proto) || WAN_PROTO="" | |
| [ -n "$WAN_PROTO" ] || die "no 'wan' interface found in /etc/config/network" | |
| if [ "$WAN_PROTO" != "pppoe" ] && [ "$FORCE" != "1" ]; then | |
| fail "WAN protocol is '$WAN_PROTO', not 'pppoe'." | |
| fail "This script is written for PPPoE WAN. A DHCP WAN normally gets IPv6" | |
| fail "through the separate 'wan6' interface, which this script disables." | |
| die "Re-run with --force only if you know what you are doing." | |
| fi | |
| HAVE_JSONFILTER=0 | |
| command -v jsonfilter >/dev/null 2>&1 && HAVE_JSONFILTER=1 | |
| printf '\n' | |
| info "Photonicat 2 — IPv6 over PPPoE setup" | |
| info "Wiki: https://photonicat.com/wiki/Photoincat_2_IPv6_PPPoE" | |
| info "Current config: wan.proto=$WAN_PROTO wan.ipv6=$(uci -q get network.wan.ipv6 || echo '(unset)') lan.ip6assign=$(uci -q get network.lan.ip6assign || echo '(unset)') lan.ip6class=$(uci -q get network.lan.ip6class || echo '(unset)')" | |
| printf '\n' | |
| # ---------- backup ----------------------------------------------------------- | |
| STAMP=$(date +%Y%m%d_%H%M%S) | |
| BACKUP_DIR="/tmp/pcat2_ipv6_backup_$STAMP" | |
| mkdir -p "$BACKUP_DIR" || die "cannot create backup dir $BACKUP_DIR" | |
| cp /etc/config/network "$BACKUP_DIR/network" || die "backup of /etc/config/network failed" | |
| cp /etc/config/dhcp "$BACKUP_DIR/dhcp" || die "backup of /etc/config/dhcp failed" | |
| [ -f /etc/docker/daemon.json ] && cp /etc/docker/daemon.json "$BACKUP_DIR/daemon.json" | |
| ok "Backed up config to $BACKUP_DIR" | |
| # ---------- step 1: WAN (PPPoE) ---------------------------------------------- | |
| uci set network.wan.ipv6='auto' | |
| ok "WAN: request IPv6 address + delegated prefix over PPPoE (network.wan.ipv6='auto')" | |
| if uci -q get network.wan6 >/dev/null; then | |
| uci set network.wan6.auto='0' | |
| ok "WAN: disabled legacy wan6 interface (network.wan6.auto='0')" | |
| fi | |
| # ---------- step 2: LAN -------------------------------------------------------- | |
| uci set network.lan.ip6assign='60' | |
| ok "LAN: assign a /60 from the delegated prefix (network.lan.ip6assign='60')" | |
| if uci -q get network.lan.ip6class >/dev/null; then | |
| uci -q delete network.lan.ip6class | |
| ok "LAN: removed ip6class — with ip6class='local' the LAN only ever gets ULA (fd00::/8) and clients never reach the IPv6 internet" | |
| fi | |
| # ---------- step 3: RA / DHCPv6 (SLAAC) ---------------------------------------- | |
| uci set dhcp.lan.ra='hybrid' | |
| uci set dhcp.lan.dhcpv6='hybrid' | |
| ok "LAN: router advertisements + DHCPv6 in hybrid mode" | |
| if [ "$RA_HIGH" = "1" ]; then | |
| uci set dhcp.lan.ra_preference='high' | |
| ok "LAN: RA preference set to high (--ra-high)" | |
| fi | |
| # ---------- apply --------------------------------------------------------------- | |
| CH_NET=$(uci changes network) | |
| CH_DHCP=$(uci changes dhcp) | |
| if [ -z "$CH_NET" ] && [ -z "$CH_DHCP" ]; then | |
| ok "Configuration was already correct — nothing to commit, skipping network restart" | |
| else | |
| if [ -n "$CH_NET" ]; then | |
| printf '\n'; info "Applying network changes:" | |
| printf '%s\n' "$CH_NET" | sed 's/^/ /' | |
| uci commit network | |
| warn "Restarting WAN — the PPPoE session will drop for a few seconds..." | |
| ifup wan | |
| ifup lan | |
| fi | |
| if [ -n "$CH_DHCP" ]; then | |
| printf '\n'; info "Applying DHCP/RA changes:" | |
| printf '%s\n' "$CH_DHCP" | sed 's/^/ /' | |
| uci commit dhcp | |
| /etc/init.d/odhcpd reload | |
| fi | |
| ok "Configuration committed and applied" | |
| fi | |
| # ---------- step 4: Docker ip6tables fix (26.04+) -------------------------------- | |
| DAEMON_JSON="/etc/docker/daemon.json" | |
| if [ -x /etc/init.d/docker ]; then | |
| DOCKER_CHANGED=0 | |
| if [ -f "$DAEMON_JSON" ] && grep -q '"ip6tables"' "$DAEMON_JSON"; then | |
| ok "Docker: $DAEMON_JSON already sets \"ip6tables\" — leaving it alone" | |
| elif [ ! -f "$DAEMON_JSON" ] || ! grep -q '"' "$DAEMON_JSON"; then | |
| mkdir -p /etc/docker | |
| printf '{\n "ip6tables": false\n}\n' > "$DAEMON_JSON" | |
| DOCKER_CHANGED=1 | |
| elif command -v python3 >/dev/null 2>&1; then | |
| # Safest path: merge the key without touching data-root or anything else | |
| if python3 - "$DAEMON_JSON" <<'PYEOF' | |
| import json, sys | |
| path = sys.argv[1] | |
| with open(path) as f: | |
| cfg = json.load(f) | |
| cfg["ip6tables"] = False | |
| with open(path, "w") as f: | |
| json.dump(cfg, f, indent=2) | |
| f.write("\n") | |
| PYEOF | |
| then | |
| DOCKER_CHANGED=1 | |
| else | |
| [ -f "$BACKUP_DIR/daemon.json" ] && cp "$BACKUP_DIR/daemon.json" "$DAEMON_JSON" | |
| warn "Docker: could not parse $DAEMON_JSON — restored backup." | |
| warn "Docker: add \"ip6tables\": false to it by hand, then: /etc/init.d/docker restart" | |
| fi | |
| else | |
| # No python3: insert the key right after the opening brace, keep the rest | |
| awk '!done && sub(/\{/, "{\n \"ip6tables\": false,") { done = 1 } { print }' \ | |
| "$DAEMON_JSON" > "$DAEMON_JSON.tmp" && mv "$DAEMON_JSON.tmp" "$DAEMON_JSON" | |
| if grep -q '"ip6tables"' "$DAEMON_JSON"; then | |
| DOCKER_CHANGED=1 | |
| else | |
| warn "Docker: automatic edit of $DAEMON_JSON failed." | |
| warn "Docker: add \"ip6tables\": false to it by hand, then: /etc/init.d/docker restart" | |
| fi | |
| fi | |
| if [ "$DOCKER_CHANGED" = "1" ]; then | |
| ok "Docker: set \"ip6tables\": false in $DAEMON_JSON (other keys untouched)" | |
| if /etc/init.d/docker running 2>/dev/null; then | |
| info "Docker: restarting daemon..." | |
| /etc/init.d/docker restart | |
| else | |
| info "Docker: daemon not running — change applies on next start" | |
| fi | |
| fi | |
| else | |
| info "Docker not installed — skipping the docker ip6tables fix" | |
| fi | |
| # ---------- step 5: verify ------------------------------------------------------- | |
| wan6_prefix() { | |
| if [ "$HAVE_JSONFILTER" = "1" ]; then | |
| ifstatus wan_6 2>/dev/null | jsonfilter -e '@["ipv6-prefix"][0].address' 2>/dev/null | |
| else | |
| ifstatus wan_6 2>/dev/null | sed -n '/"ipv6-prefix"/,/\]/p' | \ | |
| grep '"address"' | head -n1 | cut -d'"' -f4 | |
| fi | |
| } | |
| wan6_prefix_mask() { | |
| if [ "$HAVE_JSONFILTER" = "1" ]; then | |
| ifstatus wan_6 2>/dev/null | jsonfilter -e '@["ipv6-prefix"][0].mask' 2>/dev/null | |
| else | |
| ifstatus wan_6 2>/dev/null | sed -n '/"ipv6-prefix"/,/\]/p' | \ | |
| grep '"mask"' | head -n1 | tr -cd '0-9' | |
| fi | |
| } | |
| lan_assigned() { | |
| if [ "$HAVE_JSONFILTER" = "1" ]; then | |
| ifstatus lan 2>/dev/null | jsonfilter -e '@["ipv6-prefix-assignment"][*].address' 2>/dev/null | |
| else | |
| ifstatus lan 2>/dev/null | grep -A20 '"ipv6-prefix-assignment"' | \ | |
| grep '"address"' | cut -d'"' -f4 | |
| fi | |
| } | |
| printf '\n' | |
| info "Waiting for the ISP to delegate an IPv6 prefix (up to 60 s)..." | |
| WAITED=0 | |
| PREFIX="" | |
| while [ "$WAITED" -lt 60 ]; do | |
| PREFIX=$(wan6_prefix) | |
| [ -n "$PREFIX" ] && break | |
| sleep 2 | |
| WAITED=$((WAITED + 2)) | |
| done | |
| RESULT=0 | |
| if [ -n "$PREFIX" ]; then | |
| ok "Got delegated prefix: $PREFIX/$(wan6_prefix_mask)" | |
| else | |
| fail "No delegated IPv6 prefix on wan_6 after 60 s." | |
| fail "Check: is the PPPoE session up? ifstatus wan" | |
| fail "Check: DHCPv6 solicit log: logread | grep -i -e dhcpv6 -e odhcpd" | |
| fail "Your ISP may not offer IPv6 on this line, or may need it enabled on the account." | |
| RESULT=1 | |
| fi | |
| GLOBAL_ON_LAN=0 | |
| for p in $(lan_assigned); do | |
| case "$p" in | |
| fd*|fc*) ;; # ULA — doesn't count | |
| *) GLOBAL_ON_LAN=1 | |
| ok "LAN (br-lan) holds global prefix: $p" | |
| ;; | |
| esac | |
| done | |
| if [ "$GLOBAL_ON_LAN" = "0" ] && [ "$RESULT" = "0" ]; then | |
| warn "LAN has no global IPv6 prefix yet (only ULA). It can take a few more" | |
| warn "seconds — check later with: ifstatus lan | grep -A8 ipv6-prefix-assignment" | |
| fi | |
| if [ "$DO_PING" = "1" ] && [ "$RESULT" = "0" ]; then | |
| info "Testing IPv6 connectivity (ping photonicat.com)..." | |
| if ping -6 -c 3 -W 5 photonicat.com >/dev/null 2>&1 || \ | |
| ping6 -c 3 photonicat.com >/dev/null 2>&1; then | |
| ok "IPv6 internet is reachable (photonicat.com answered over IPv6)" | |
| elif ping -6 -c 3 -W 5 2400:3200::1 >/dev/null 2>&1 || \ | |
| ping6 -c 3 2400:3200::1 >/dev/null 2>&1; then | |
| warn "IPv6 routing works (2400:3200::1 answers) but the DNS test target" | |
| warn "did not — likely just missing AAAA/DNS, not a connectivity problem." | |
| else | |
| fail "IPv6 ping failed. Wait ~30 s and retry: ping -6 photonicat.com" | |
| RESULT=1 | |
| fi | |
| fi | |
| # ---------- summary ------------------------------------------------------------- | |
| printf '\n' | |
| if [ "$RESULT" = "0" ]; then | |
| ok "Done. Devices on the LAN should now get global IPv6 addresses." | |
| info "Check on a client: ip -6 addr show and ping -6 photonicat.com" | |
| info "(Reconnect Wi-Fi clients if they don't pick up an address right away.)" | |
| else | |
| warn "Configuration was applied, but verification did not (yet) succeed." | |
| warn "PPPoE + prefix delegation can take a minute; re-run this script to re-check." | |
| fi | |
| info "Backup kept at $BACKUP_DIR — to restore:" | |
| info " cp $BACKUP_DIR/network /etc/config/network" | |
| info " cp $BACKUP_DIR/dhcp /etc/config/dhcp" | |
| [ -f "$BACKUP_DIR/daemon.json" ] && info " cp $BACKUP_DIR/daemon.json /etc/docker/daemon.json" | |
| info " then: ifup wan; ifup lan; /etc/init.d/odhcpd reload" | |
| exit "$RESULT" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment