#!/usr/bin/env bash set -euo pipefail ACTION="${1:-start}" APP_ENV="/etc/runtipi/app-data/runtipi/rego-tunnel/app.env" if [[ -f "$APP_ENV" ]]; then # shellcheck disable=SC1090 source "$APP_ENV" fi CONTAINER_NAME="${CONTAINER_NAME:-rego-tunnel_runtipi-rego-tunnel-1}" # Prefer the user-config network (stable host bridge name) if it exists. NET_NAME="${NET_NAME:-}" NET_NAME_FALLBACK="${NET_NAME_FALLBACK:-rego-tunnel_runtipi_network}" NET_NAME_PREFERRED="${NET_NAME_PREFERRED:-rego-tunnel_runtipi_vpn_static}" TARGET_IP="${TARGET_IP:-10.35.33.230}" HOST_BRIDGE_ALIAS="${HOST_BRIDGE_ALIAS:-br-rego-tunnel}" VM_SUBNET="${VM_SUBNET:-100.100.0.0}" TARGET_CIDR="$TARGET_IP/32" VM_SUBNET_CIDR="${VM_SUBNET_CIDR:-${VM_SUBNET}/24}" log() { echo "[rego-routing] $*" >&2 } remove_stale_nft_redirects() { # Some previous setups may have installed a transparent proxy (REDSOCKS) # redirecting TCP destined for TARGET_IP to a local port (e.g. 12345). # That breaks the "single target IP via container" gateway model. # # We only remove rules that match our TARGET_IP in the ip nat/REDSOCKS chain. if ! command -v nft >/dev/null 2>&1; then return 0 fi local handles handles="$(nft -a list chain ip nat REDSOCKS 2>/dev/null | awk -v ip="$TARGET_IP" '/ip daddr/ && $0 ~ ip && /redirect to :/ {for(i=1;i<=NF;i++) if($i=="handle") print $(i+1)}' || true)" if [[ -z "$handles" ]]; then return 0 fi while read -r h; do [[ -n "$h" ]] || continue nft delete rule ip nat REDSOCKS handle "$h" 2>/dev/null || true done <<< "$handles" } remove_stale_host_routes() { # The VM's internal subnet (default 100.100.0.0/24) should live only inside # the rego-tunnel container/VM. If the host has a route for it (left over from # older scripts), it can cause confusing routing. ip -4 route del "$VM_SUBNET_CIDR" 2>/dev/null || true } get_lan_if() { # Avoid early-exit pipelines (pipefail + SIGPIPE) by not exiting awk early. ip route show default 0.0.0.0/0 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1)}}' } choose_net_name() { if [[ -n "${NET_NAME:-}" ]]; then echo "$NET_NAME" return 0 fi if docker network inspect "$NET_NAME_PREFERRED" >/dev/null 2>&1; then echo "$NET_NAME_PREFERRED" return 0 fi echo "$NET_NAME_FALLBACK" } get_bridge_and_container_ip() { local chosen net_id bridge_opt bridge cip chosen="$(choose_net_name)" net_id="$(docker network inspect -f '{{.Id}}' "$chosen" 2>/dev/null || true)" if [[ -z "$net_id" ]]; then return 1 fi # If Docker network specifies an explicit bridge name, use it. bridge_opt="$(docker network inspect -f '{{index .Options "com.docker.network.bridge.name"}}' "$chosen" 2>/dev/null || true)" if [[ -n "$bridge_opt" && "$bridge_opt" != "" ]]; then bridge="$bridge_opt" else bridge="br-${net_id:0:12}" fi cip="$(docker inspect -f "{{(index .NetworkSettings.Networks \"$chosen\").IPAddress}}" "$CONTAINER_NAME" 2>/dev/null || true)" if [[ -z "$cip" ]]; then return 1 fi echo "$bridge $cip" } set_bridge_alias() { local bridge="$1" # Docker creates the bridge name (br-); renaming it breaks Docker. # Setting an alias is safe and improves readability in `ip link` output. if [[ -n "${HOST_BRIDGE_ALIAS:-}" ]]; then ip link set dev "$bridge" alias "$HOST_BRIDGE_ALIAS" 2>/dev/null || true fi } container_apply() { docker exec "$CONTAINER_NAME" sh -lc 'set -e BR="${BRIDGE_NAME:-br-rego-vpn}" VM="${VM_NET_IP:-100.100.0.2}" TARGET="${TARGET_IP:-10.35.33.230}" echo 1 > /proc/sys/net/ipv4/ip_forward ip route replace "$TARGET/32" via "$VM" dev "$BR" 2>/dev/null || ip route replace "$TARGET" via "$VM" dev "$BR" 2>/dev/null || true iptables -C FORWARD -d "$TARGET" -j ACCEPT 2>/dev/null || iptables -A FORWARD -d "$TARGET" -j ACCEPT iptables -C FORWARD -s "$TARGET" -j ACCEPT 2>/dev/null || iptables -A FORWARD -s "$TARGET" -j ACCEPT iptables -t nat -C POSTROUTING -o "$BR" -d "$TARGET" -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -o "$BR" -d "$TARGET" -j MASQUERADE ' } container_remove() { docker exec "$CONTAINER_NAME" sh -lc 'set -e BR="${BRIDGE_NAME:-br-rego-vpn}" VM="${VM_NET_IP:-100.100.0.2}" TARGET="${TARGET_IP:-10.35.33.230}" ip route del "$TARGET/32" via "$VM" dev "$BR" 2>/dev/null || true iptables -t nat -D POSTROUTING -o "$BR" -d "$TARGET" -j MASQUERADE 2>/dev/null || true ' || true } apply_host_rules() { local lan_if bridge cip remove_stale_nft_redirects remove_stale_host_routes lan_if="$(get_lan_if)" if [[ -z "$lan_if" ]]; then log "WARN: could not detect LAN default interface; skipping host iptables" fi read -r bridge cip < <(get_bridge_and_container_ip) set_bridge_alias "$bridge" echo 1 > /proc/sys/net/ipv4/ip_forward ip route replace "$TARGET_CIDR" via "$cip" dev "$bridge" if [[ -n "${lan_if:-}" ]]; then iptables -C DOCKER-USER -i "$lan_if" -o "$bridge" -d "$TARGET_CIDR" -j ACCEPT 2>/dev/null || \ iptables -I DOCKER-USER 1 -i "$lan_if" -o "$bridge" -d "$TARGET_CIDR" -j ACCEPT iptables -C DOCKER-USER -i "$bridge" -o "$lan_if" -s "$TARGET_CIDR" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ iptables -I DOCKER-USER 1 -i "$bridge" -o "$lan_if" -s "$TARGET_CIDR" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT fi container_apply log "OK: routing $TARGET_CIDR via $cip ($bridge)" } remove_host_rules() { local lan_if bridge cip lan_if="$(get_lan_if)" if read -r bridge cip < <(get_bridge_and_container_ip); then ip route del "$TARGET_CIDR" via "$cip" dev "$bridge" 2>/dev/null || true if [[ -n "${lan_if:-}" ]]; then iptables -D DOCKER-USER -i "$lan_if" -o "$bridge" -d "$TARGET_CIDR" -j ACCEPT 2>/dev/null || true iptables -D DOCKER-USER -i "$bridge" -o "$lan_if" -s "$TARGET_CIDR" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true fi fi container_remove } case "$ACTION" in start) # Wait briefly for docker + container/network to be ready. for _ in {1..30}; do if get_bridge_and_container_ip >/dev/null 2>&1; then apply_host_rules exit 0 fi sleep 2 done log "ERROR: container/network not ready; no routing applied" exit 0 ;; stop) remove_host_rules ;; *) echo "Usage: $0 {start|stop}" >&2 exit 2 ;; esac