feat(install): probe-then-choose IPv6 mode (prefix / pool / v4)
00-ip-ask.sh now runs the unchanged check-ipv6-support.sh. Whole prefix verified -> classic prefix mode. Not verified but >1 global address on the NIC -> list them, keep the first for the host, ask the user to confirm the rest as the pool. Declined or no addresses -> pure IPv4. ndppd is only installed in prefix mode; pool mode uses kernel proxy_ndp. vps ip6 gains neigh-add-proxy for the per-address wiring. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
1 parent
56b1a85412
commit
5a0494df3c
3 files changed
+265
-7
No files matched your search
+7
-5
@@ -42,12 +42,14 @@ echo "===== 00-ip-ask ====="
|
||||
source "$ROOT/scripts/00-ip-ask.sh" || { echo "error: install-time network asks failed — aborting" >&2; exit 1; }
|
||||
export VPSMGR_IPV6_SUBNET="${VPSMGR_IPV6_SUBNET:-}"
|
||||
export VPSMGR_IPV4_SUBNET="${VPSMGR_IPV4_SUBNET:-}"
|
||||
export VPSMGR_IPV6_MODE="${VPSMGR_IPV6_MODE:-}"
|
||||
export VPSMGR_IPV6_POOL="${VPSMGR_IPV6_POOL:-}"
|
||||
|
||||
# NDP proxy for IPv6 pass-through: each container owns a /112 block and ndppd
|
||||
# relays neighbor discovery on the upstream interface for it, so every address
|
||||
# a container binds is reachable from the internet. Only needed when IPv6 is
|
||||
# enabled; small, no data-plane involvement.
|
||||
if [[ -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
# NDP proxy for IPv6 pass-through. Prefix mode: each container owns a /112
|
||||
# block and ndppd relays neighbor discovery on the upstream interface for it.
|
||||
# Pool mode: kernel proxy_ndp handles each /128 (no ndppd needed). Only
|
||||
# needed in prefix mode; small, no data-plane involvement.
|
||||
if [[ "${VPSMGR_IPV6_MODE:-}" == "prefix" && -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
echo
|
||||
echo "===== installing ndppd (IPv6 NDP proxy) ====="
|
||||
apt-get update -qq 2>/dev/null || true
|
||||
|
||||
+250
-1
@@ -39,7 +39,21 @@ if ! command -v python3 >/dev/null 2>&1; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- IPv6 prefix -------------------------------------------------------------
|
||||
# --- IPv6 pass-through -------------------------------------------------------
|
||||
#
|
||||
# The installer offers THREE IPv6 outcomes, decided by probing the host with
|
||||
# the (unchanged) check-ipv6-support.sh script:
|
||||
#
|
||||
# 1. prefix — the provider routes a whole prefix (verified from outside):
|
||||
# containers get deterministic /112 blocks (classic mode).
|
||||
# 2. pool — no routable whole prefix, but the host has multiple global
|
||||
# addresses on its NIC (the "discrete whitelist" providers):
|
||||
# containers each get one address from the pool the user
|
||||
# confirms (the host keeps the first address for itself).
|
||||
# 3. none — pure IPv4 (user cancels, or no global IPv6 at all).
|
||||
#
|
||||
# The chosen mode is exported as VPSMGR_IPV6_MODE (none|prefix|pool) and the
|
||||
# pool (if any) as VPSMGR_IPV6_POOL; the config's FillAuto reads both.
|
||||
|
||||
# validate_prefix: exit 0 if arg is a global IPv6 CIDR (/80 or shorter) with
|
||||
# an explicit prefix length.
|
||||
@@ -61,6 +75,241 @@ if a.is_private or a.is_link_local or a.is_loopback or a.is_unspecified:
|
||||
PY
|
||||
}
|
||||
|
||||
# is_global_v6: exit 0 if arg is a bare GLOBAL (non-ULA, non-link-local,
|
||||
# non-loopback, non-unspecified) IPv6 address without a prefix length.
|
||||
is_global_v6(){
|
||||
python3 - "$1" <<'PY'
|
||||
import ipaddress, sys
|
||||
try:
|
||||
a = ipaddress.IPv6Address(sys.argv[1])
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
if a.is_private or a.is_link_local or a.is_loopback or a.is_unspecified or a.is_multicast:
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
# run_detector — run the (unchanged) check-ipv6-support.sh and capture its
|
||||
# verdict. Prints "prefix" when the whole prefix was verified reachable from
|
||||
# outside, "noverify" when no external verification was possible (offline /
|
||||
# rate-limited), "unverified" when the random test address got no reply (the
|
||||
# prefix is NOT routed as a whole). The script's own output is streamed to the
|
||||
# user so the installer keeps the same look; only the verdict line is parsed.
|
||||
run_detector(){
|
||||
local det
|
||||
det="${ROOT:-$(dirname "$(readlink -f "$0")")}/check-ipv6-support.sh"
|
||||
if [[ ! -x "$det" ]]; then
|
||||
log "check-ipv6-support.sh not found at $det — skipping automatic detection"
|
||||
echo "unverified"
|
||||
return 0
|
||||
fi
|
||||
local out
|
||||
out=$("$det" 2>&1 || true)
|
||||
printf '%s\n' "$out" >&2
|
||||
if grep -q "Pass-through:[[:space:]]*VERIFIED" <<< "$out"; then
|
||||
echo "prefix"
|
||||
elif grep -q "UNKNOWN (run again" <<< "$out"; then
|
||||
echo "noverify"
|
||||
else
|
||||
echo "unverified"
|
||||
fi
|
||||
}
|
||||
|
||||
# ask_ipv6 — the mode-selection interview. Sets VPSMGR_IPV6_MODE and (for
|
||||
# pool) VPSMGR_IPV6_POOL; leaves them empty for pure IPv4.
|
||||
ask_ipv6(){
|
||||
# --- env override: VPSMGR_IPV6_MODE=none|prefix|pool (with pool list) ---
|
||||
if [[ -n "${VPSMGR_IPV6_MODE:-}" ]]; then
|
||||
case "$VPSMGR_IPV6_MODE" in
|
||||
none) log "IPv6 mode: none (from env)"; export VPSMGR_IPV6_MODE=none; return 0 ;;
|
||||
prefix)
|
||||
if [[ -z "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
die "VPSMGR_IPV6_MODE=prefix requires VPSMGR_IPV6_SUBNET (e.g. 2602:fada:6::/64)"
|
||||
return 1
|
||||
fi
|
||||
log "IPv6 mode: prefix $VPSMGR_IPV6_SUBNET (from env)"; return 0 ;;
|
||||
pool)
|
||||
if [[ -z "${VPSMGR_IPV6_POOL:-}" ]]; then
|
||||
die "VPSMGR_IPV6_MODE=pool requires VPSMGR_IPV6_POOL (comma-separated global addresses)"
|
||||
return 1
|
||||
fi
|
||||
log "IPv6 mode: pool (from env)"; return 0 ;;
|
||||
*) die "VPSMGR_IPV6_MODE must be none|prefix|pool (got '$VPSMGR_IPV6_MODE')"; return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- reinstall adoption: keep the previous mode ---
|
||||
if [[ -f /etc/vpsmgr/config.yaml ]]; then
|
||||
local EXISTING
|
||||
EXISTING=$(grep -E '^\s+ipv6_mode:' /etc/vpsmgr/config.yaml 2>/dev/null | awk -F': ' '{print $2}' | tr -d '"')
|
||||
if [[ -n "$EXISTING" ]]; then
|
||||
log "existing config has ipv6_mode=$EXISTING — keeping it"
|
||||
export VPSMGR_IPV6_MODE="$EXISTING"
|
||||
if [[ "$EXISTING" == "pool" ]]; then
|
||||
local POOL
|
||||
POOL=$(grep -A200 '^\s+ipv6_pool:' /etc/vpsmgr/config.yaml 2>/dev/null | sed -n 's/^\s*-\s*//p' | tr '\n' ',' | sed 's/,$//')
|
||||
[[ -n "$POOL" ]] && export VPSMGR_IPV6_POOL="$POOL"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
# Older config without ipv6_mode: fall back to ipv6_subnet presence.
|
||||
if grep -Eq '^\s+ipv6_subnet:' /etc/vpsmgr/config.yaml 2>/dev/null; then
|
||||
local OLDSUB
|
||||
OLDSUB=$(grep -E '^\s+ipv6_subnet:' /etc/vpsmgr/config.yaml 2>/dev/null | awk -F': ' '{print $2}' | tr -d '"')
|
||||
if [[ -n "$OLDSUB" ]]; then
|
||||
log "existing config has ipv6_subnet=$OLDSUB — keeping prefix mode"
|
||||
export VPSMGR_IPV6_MODE=prefix VPSMGR_IPV6_SUBNET="$OLDSUB"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- non-interactive with no env var: IPv6 stays disabled ---
|
||||
if [[ ! -t 0 ]] && [[ -z "${FORCE_ASK:-}" ]]; then
|
||||
log "non-interactive install, no VPSMGR_IPV6_MODE set — IPv6 disabled"
|
||||
export VPSMGR_IPV6_MODE=none
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo " IPv6 pass-through — BETA / 实验性功能"
|
||||
echo "------------------------------------------------------------"
|
||||
echo " Each container gets its own public IPv6 address (no NAT)."
|
||||
echo " Requires either a routable prefix OR multiple global"
|
||||
echo " addresses on this host's NIC."
|
||||
echo " 每台小鸡将获得独立的公网 IPv6 地址(无 NAT)。"
|
||||
echo " 需要可路由前缀,或本机网卡上的多个公网地址。"
|
||||
echo " Default: DISABLED. Only enable if you understand the risks."
|
||||
echo " 默认不启用,请确认理解后再开启。"
|
||||
echo "============================================================"
|
||||
echo
|
||||
read -r -p "Enable IPv6 pass-through? 启用 IPv6 直通? [y/N] " ans
|
||||
case "${ans,,}" in
|
||||
y|yes) ;;
|
||||
*) log "IPv6 pass-through disabled / 未启用"; export VPSMGR_IPV6_MODE=none; return 0 ;;
|
||||
esac
|
||||
|
||||
# Run the detector (unchanged script) to learn the host's capability.
|
||||
log "running check-ipv6-support.sh to probe this host..."
|
||||
DET=$(run_detector)
|
||||
log "detector verdict: $DET"
|
||||
|
||||
if [[ "$DET" == "prefix" ]]; then
|
||||
# Whole prefix routed -> classic mode. Default candidate from the host's
|
||||
# own global address, same as before.
|
||||
local EXT_IF GLOBAL CAND
|
||||
EXT_IF=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
||||
GLOBAL=$(ip -6 -o addr show dev "$EXT_IF" scope global 2>/dev/null | awk '{print $4; exit}')
|
||||
CAND=""
|
||||
if [[ -n "$GLOBAL" ]]; then
|
||||
local GADDR GLEN
|
||||
GADDR="${GLOBAL%%/*}"
|
||||
GLEN="${GLOBAL##*/}"
|
||||
GLEN="${GLEN:-64}"
|
||||
CAND=$(python3 -c 'import ipaddress,sys
|
||||
a=ipaddress.IPv6Address(sys.argv[1])
|
||||
plen=int(sys.argv[2])
|
||||
n=ipaddress.IPv6Network((int(a), plen), strict=False)
|
||||
print(n.network_address)' "$GADDR" "$GLEN")
|
||||
CAND="$CAND/$GLEN"
|
||||
fi
|
||||
local PREFIX
|
||||
if [[ -n "$CAND" ]]; then
|
||||
log "detected host global address: $GLOBAL"
|
||||
read -r -p "Global prefix for containers — include the length (e.g. /64, /80) [default: $CAND]: " PREFIX
|
||||
PREFIX="${PREFIX:-$CAND}"
|
||||
else
|
||||
read -r -p "Global prefix for containers — include the length (e.g. 2001:db8::/64; up to /80): " PREFIX
|
||||
fi
|
||||
PREFIX="${PREFIX%$'\r'}"
|
||||
local PREFIX_NORM
|
||||
PREFIX_NORM=$(python3 - "$PREFIX" <<'PY'
|
||||
import ipaddress, sys
|
||||
p = sys.argv[1]
|
||||
if "/" not in p:
|
||||
sys.exit(1)
|
||||
try:
|
||||
print(ipaddress.IPv6Network(p, strict=False))
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
PY
|
||||
)
|
||||
if validate_prefix "$PREFIX" && [[ -n "$PREFIX_NORM" ]]; then
|
||||
export VPSMGR_IPV6_MODE=prefix VPSMGR_IPV6_SUBNET="$PREFIX_NORM"
|
||||
log "IPv6 mode: prefix $PREFIX_NORM"
|
||||
else
|
||||
die "invalid prefix '$PREFIX' — must be a global IPv6 CIDR with an explicit length (e.g. 2602:fada:6::/64, or a /80 like 2406:da14:1dd2:a807:753a::/80)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# No verified whole prefix. Count the host's GLOBAL addresses: more than one
|
||||
# means pool mode is possible (one stays with the host, the rest go in the
|
||||
# pool). Private/ULA/link-local addresses are excluded.
|
||||
local EXT_IF GLOBALS COUNT
|
||||
EXT_IF=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
||||
GLOBALS=$(ip -6 -o addr show dev "$EXT_IF" scope global 2>/dev/null | awk '{print $4}')
|
||||
COUNT=0
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
local a
|
||||
a="${line%%/*}"
|
||||
if is_global_v6 "$a"; then COUNT=$((COUNT+1)); fi
|
||||
done <<< "$GLOBALS"
|
||||
|
||||
if [[ "$COUNT" -le 1 ]]; then
|
||||
warn "no routable whole prefix AND only $COUNT global IPv6 address(es) — IPv6 pass-through not possible"
|
||||
log "proceeding with pure IPv4"
|
||||
export VPSMGR_IPV6_MODE=none
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Pool mode offer: show the global addresses, keep the first for the host,
|
||||
# ask the user to confirm the rest as the pool.
|
||||
log "$COUNT global IPv6 addresses found on $EXT_IF (whole-prefix routing NOT verified):"
|
||||
local first=""
|
||||
local pool=""
|
||||
local n=0
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
local a
|
||||
a="${line%%/*}"
|
||||
if ! is_global_v6 "$a"; then continue; fi
|
||||
n=$((n+1))
|
||||
if [[ $n -eq 1 ]]; then
|
||||
first="$a"
|
||||
log " [host] $a (kept for the host itself)"
|
||||
else
|
||||
log " [pool] $a"
|
||||
if [[ -z "$pool" ]]; then pool="$a"; else pool="$pool,$a"; fi
|
||||
fi
|
||||
done <<< "$GLOBALS"
|
||||
|
||||
echo
|
||||
read -r -p "Add the $((COUNT-1)) addresses above to the container pool? 将以上 $((COUNT-1)) 个地址加入小鸡地址池? [y/N] " ans2
|
||||
case "${ans2,,}" in
|
||||
y|yes) ;;
|
||||
*) log "address pool declined — proceeding with pure IPv4"; export VPSMGR_IPV6_MODE=none; return 0 ;;
|
||||
esac
|
||||
|
||||
# Validate every pool address once more before exporting.
|
||||
local ok=1
|
||||
IFS=',' read -r -a pool_arr <<< "$pool"
|
||||
for a in "${pool_arr[@]}"; do
|
||||
if ! is_global_v6 "$a"; then ok=0; break; fi
|
||||
done
|
||||
if [[ "$ok" -ne 1 || -z "$pool" ]]; then
|
||||
die "invalid pool addresses gathered ($pool)"
|
||||
return 1
|
||||
fi
|
||||
export VPSMGR_IPV6_MODE=pool VPSMGR_IPV6_POOL="$pool"
|
||||
log "IPv6 mode: pool ($((COUNT-1)) addresses)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- container subnet --------------------------------------------------------
|
||||
|
||||
# validate_octet: exit 0 if arg is an integer 1..254.
|
||||
|
||||
+8
-1
@@ -26,12 +26,13 @@ import (
|
||||
// addr-add <ipv6-addr/prefix> <iface> ip -6 addr add <addr> dev <iface>
|
||||
// route-del <ipv6-addr> <iface> ip -6 route del <addr>/128 dev <iface>
|
||||
// neigh-del-proxy <ipv6-addr> <iface> ip -6 neigh del proxy <addr> dev <iface>
|
||||
// neigh-add-proxy <ipv6-addr> <iface> ip -6 neigh add proxy <addr> dev <iface>
|
||||
//
|
||||
// Every non-op argument must be a valid IPv6 address/CIDR and a valid Linux
|
||||
// interface name — nothing else can reach `ip`.
|
||||
func cmdIP6(args []string) error {
|
||||
if len(args) != 3 {
|
||||
return fmt.Errorf("usage: vps ip6 <route-add|addr-add|route-del|neigh-del-proxy> <addr-or-cidr> <iface>")
|
||||
return fmt.Errorf("usage: vps ip6 <route-add|addr-add|route-del|neigh-del-proxy|neigh-add-proxy> <addr-or-cidr> <iface>")
|
||||
}
|
||||
op, val, dev := args[0], args[1], args[2]
|
||||
|
||||
@@ -77,6 +78,12 @@ func cmdIP6(args []string) error {
|
||||
return fmt.Errorf("neigh-del-proxy: %q is not an IPv6 address", val)
|
||||
}
|
||||
return runIP("-6", "neigh", "del", "proxy", val, "dev", dev)
|
||||
case "neigh-add-proxy":
|
||||
ip := net.ParseIP(val)
|
||||
if ip == nil || ip.To4() != nil {
|
||||
return fmt.Errorf("neigh-add-proxy: %q is not an IPv6 address", val)
|
||||
}
|
||||
return runIP("-6", "neigh", "add", "proxy", val, "dev", dev)
|
||||
default:
|
||||
return fmt.Errorf("unknown ip6 operation %q", op)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user