#!/bin/sh WARP_CONF="${WARP_CONF:-/etc/sing-box-warp/warp.conf}" PROTON_CONF="${PROTON_CONF:-/etc/sing-box-warp/proton.conf}" OUTPUT_CONFIG="${OUTPUT_CONFIG:-/opt/sing-box-warp/config.json}" ENABLE_TUN_FILE="${ENABLE_TUN_FILE:-/etc/sing-box-warp/enable-tun}" DEFAULT_ROUTE_FILE="${DEFAULT_ROUTE_FILE:-/etc/sing-box-warp/default-route}" load_enable_tun() { local value value=$(trim "${ENABLE_TUN:-}") if [ -z "$value" ] && [ -f "$ENABLE_TUN_FILE" ]; then value=$(trim "$(cat "$ENABLE_TUN_FILE")") fi case "$value" in 1|yes|true|y|Y|on|ON) ENABLE_TUN=1 ;; *) ENABLE_TUN=0 ;; esac } # Where all non-SOCKS traffic goes (TUN rules, routing lists, IP-leak sites). # The SOCKS inbounds keep their own hard-coded outbound. load_default_route() { local value value=$(trim "${DEFAULT_ROUTE:-}") if [ -z "$value" ] && [ -f "$DEFAULT_ROUTE_FILE" ]; then value=$(trim "$(cat "$DEFAULT_ROUTE_FILE")") fi case "$value" in 2|proton|PROTON|Proton) DEFAULT_ROUTE=proton ;; *) DEFAULT_ROUTE=warp ;; esac } urldecode() { echo "$1" | sed 's/%3[dD]/=/g; s/%2[bB]/+/g; s/%2[fF]/\//g; s/%2[cC]/,/g' } trim() { echo "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' } # Default-route NIC (eth0, ens3, enp0s3, …); override with TUN_EXCLUDE_INTERFACE detect_default_interface() { local iface iface=$(trim "${TUN_EXCLUDE_INTERFACE:-}") if [ -n "$iface" ]; then echo "$iface" return 0 fi if command -v ip >/dev/null 2>&1; then iface=$(ip -4 route show default 2>/dev/null \ | awk '/default/ { for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }') if [ -n "$iface" ]; then echo "$iface" return 0 fi iface=$(ip -4 route get 1.1.1.1 2>/dev/null \ | awk '{ for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }') if [ -n "$iface" ]; then echo "$iface" return 0 fi fi if [ -r /proc/net/route ]; then iface=$(awk '$2 == "00000000" && $1 != "Iface" { print $1; exit }' /proc/net/route) if [ -n "$iface" ]; then echo "$iface" return 0 fi fi return 1 } normalize_cidr() { local addr local suffix addr=$(trim "$1") suffix="$2" if [ -z "$addr" ]; then echo "" return fi case "$addr" in */*) echo "$addr" ;; *) echo "${addr}/${suffix}" ;; esac } # Convert a comma-separated AllowedIPs list into a JSON array body: # "0.0.0.0/0, ::/0" -> "0.0.0.0/0", "::/0" build_allowed_ips_json() { local input local result="" local item local old_ifs input="$1" if [ -z "$input" ]; then echo '"0.0.0.0/0", "::/0"' return fi old_ifs="$IFS" IFS=',' for item in $input; do item=$(trim "$item") [ -z "$item" ] && continue if [ -z "$result" ]; then result="\"$item\"" else result="$result, \"$item\"" fi done IFS="$old_ifs" if [ -z "$result" ]; then result='"0.0.0.0/0", "::/0"' fi echo "$result" } # Extract parameter from URL get_param() { local url="$1" local param="$2" echo "$url" | sed -n "s/.*[?&]${param}=\([^&#]*\).*/\1/p" } parse_from_wg_url() { WG_URL=$(grep "^wg://" "$WARP_CONF" | head -1) if [ -z "$WG_URL" ]; then echo "Error: No wg:// URL found in $WARP_CONF" exit 1 fi SERVER=$(echo "$WG_URL" | sed 's|wg://\([^:]*\):.*|\1|') PORT=$(echo "$WG_URL" | sed 's|wg://[^:]*:\([0-9]*\)?.*|\1|') PRIVATE_KEY=$(urldecode "$(get_param "$WG_URL" "private_key")") PUBLIC_KEY=$(urldecode "$(get_param "$WG_URL" "peer_public_key")") MTU=$(get_param "$WG_URL" "mtu") LOCAL_ADDRESS=$(urldecode "$(get_param "$WG_URL" "local_address")") Jc=$(get_param "$WG_URL" "junk_packet_count") Jmin=$(get_param "$WG_URL" "junk_packet_min_size") Jmax=$(get_param "$WG_URL" "junk_packet_max_size") H1=$(get_param "$WG_URL" "init_packet_magic_header") H2=$(get_param "$WG_URL" "response_packet_magic_header") H3=$(get_param "$WG_URL" "underload_packet_magic_header") H4=$(get_param "$WG_URL" "transport_packet_magic_header") IPV4=$(echo "$LOCAL_ADDRESS" | tr ',-' '\n' | sed -n '1p') IPV6=$(echo "$LOCAL_ADDRESS" | tr ',-' '\n' | sed -n '2p') IPV4=$(normalize_cidr "$IPV4" 32) IPV6=$(normalize_cidr "$IPV6" 128) if [ -z "$IPV4" ]; then echo "Error: local_address (IPv4) is empty" exit 1 fi ADDRESS_JSON=$(printf '"%s"' "$IPV4") MTU=${MTU:-1280} Jc=${Jc:-4} Jmin=${Jmin:-40} Jmax=${Jmax:-70} H1=${H1:-1} H2=${H2:-2} H3=${H3:-3} H4=${H4:-4} ALLOWED_IPS="0.0.0.0/0" TAG="warp-out" LOG_LEVEL="error" } parse_from_ini() { local section="" local key="" local value="" while IFS= read -r line || [ -n "$line" ]; do line=$(trim "$line") [ -z "$line" ] && continue case "$line" in \#*|\;*) continue ;; esac case "$line" in "[Interface]") section="Interface"; continue ;; "[Peer]") section="Peer"; continue ;; esac key=$(trim "$(echo "$line" | cut -d'=' -f1)") value=$(trim "$(echo "$line" | cut -d'=' -f2-)") [ -z "$key" ] && continue if [ "$section" = "Interface" ]; then case "$key" in PrivateKey) PRIVATE_KEY="$value" ;; Address) IPV4=$(trim "$(echo "$value" | cut -d',' -f1)") IPV6=$(trim "$(echo "$value" | cut -d',' -f2)") ;; MTU) MTU="$value" ;; S1) S1="$value" ;; S2) S2="$value" ;; S3) S3="$value" ;; Jc) Jc="$value" ;; Jmin) Jmin="$value" ;; Jmax) Jmax="$value" ;; H1) H1="$value" ;; H2) H2="$value" ;; H3) H3="$value" ;; H4) H4="$value" ;; I1) I1="$value" ;; I2) I2="$value" ;; esac elif [ "$section" = "Peer" ]; then case "$key" in PublicKey) PUBLIC_KEY="$value" ;; AllowedIPs) if echo "$value" | grep -q "0.0.0.0/0"; then ALLOWED_IPS="0.0.0.0/0" else ALLOWED_IPS=$(trim "$(echo "$value" | cut -d',' -f1)") fi ;; Endpoint) SERVER=$(echo "$value" | cut -d':' -f1) PORT=$(echo "$value" | cut -d':' -f2) ;; esac fi done < "$WARP_CONF" IPV4=$(normalize_cidr "$IPV4" 32) IPV6=$(normalize_cidr "$IPV6" 128) if [ -z "$IPV4" ]; then echo "Error: Address (IPv4) is empty" exit 1 fi ADDRESS_JSON=$(printf '"%s"' "$IPV4") MTU=${MTU:-1280} S1=${S1:-0} S2=${S2:-0} S3=${S3:-0} Jc=${Jc:-4} Jmin=${Jmin:-40} Jmax=${Jmax:-70} H1=${H1:-1} H2=${H2:-2} H3=${H3:-3} H4=${H4:-4} ALLOWED_IPS=${ALLOWED_IPS:-0.0.0.0/0} if [ -z "$SERVER" ] || [ -z "$PORT" ]; then echo "Error: Endpoint is empty" exit 1 fi TAG="warp-out" LOG_LEVEL="error" } parse_proton_from_ini() { local section="" local key="" local value="" local p_ipv4="" local p_allowed="" while IFS= read -r line || [ -n "$line" ]; do line=$(trim "$line") [ -z "$line" ] && continue case "$line" in \#*|\;*) continue ;; esac case "$line" in "[Interface]") section="Interface"; continue ;; "[Peer]") section="Peer"; continue ;; esac key=$(trim "$(echo "$line" | cut -d'=' -f1)") value=$(trim "$(echo "$line" | cut -d'=' -f2-)") [ -z "$key" ] && continue if [ "$section" = "Interface" ]; then case "$key" in PrivateKey) PROTON_PRIVATE_KEY="$value" ;; Address) p_ipv4=$(trim "$(echo "$value" | cut -d',' -f1)") ;; MTU) PROTON_MTU="$value" ;; esac elif [ "$section" = "Peer" ]; then case "$key" in PublicKey) PROTON_PUBLIC_KEY="$value" ;; AllowedIPs) p_allowed="$value" ;; Endpoint) PROTON_SERVER=$(echo "$value" | cut -d':' -f1) PROTON_PORT=$(echo "$value" | cut -d':' -f2) ;; esac fi done < "$PROTON_CONF" PROTON_ADDRESS=$(normalize_cidr "$p_ipv4" 32) PROTON_ALLOWED_IPS=$(build_allowed_ips_json "$p_allowed") } parse_proton_from_wg_url() { local url local local_addr local p_ipv4 url=$(grep "^wg://" "$PROTON_CONF" | head -1) PROTON_SERVER=$(echo "$url" | sed 's|wg://\([^:]*\):.*|\1|') PROTON_PORT=$(echo "$url" | sed 's|wg://[^:]*:\([0-9]*\)?.*|\1|') PROTON_PRIVATE_KEY=$(urldecode "$(get_param "$url" "private_key")") PROTON_PUBLIC_KEY=$(urldecode "$(get_param "$url" "peer_public_key")") PROTON_MTU=$(get_param "$url" "mtu") local_addr=$(urldecode "$(get_param "$url" "local_address")") p_ipv4=$(echo "$local_addr" | tr ',-' '\n' | sed -n '1p') PROTON_ADDRESS=$(normalize_cidr "$p_ipv4" 32) PROTON_ALLOWED_IPS='"0.0.0.0/0", "::/0"' } parse_proton_conf() { local first_line first_line=$(head -n 1 "$PROTON_CONF" | tr -d '\r') first_line=$(trim "$first_line") if echo "$first_line" | grep -q "^wg://"; then parse_proton_from_wg_url elif echo "$first_line" | grep -q "^\[Interface\]"; then parse_proton_from_ini else echo "Error: Unsupported proton.conf format (expected wg:// or [Interface])" exit 1 fi PROTON_MTU=${PROTON_MTU:-1280} if [ -z "$PROTON_PRIVATE_KEY" ] || [ -z "$PROTON_PUBLIC_KEY" ] \ || [ -z "$PROTON_SERVER" ] || [ -z "$PROTON_PORT" ] \ || [ -z "$PROTON_ADDRESS" ]; then echo "Error: proton.conf is missing required fields (PrivateKey/PublicKey/Endpoint/Address)" exit 1 fi ENABLE_PROTON=1 } # Join non-empty JSON object strings with ",\n" separators (for arrays). join_objects() { local out="" local item for item in "$@"; do [ -z "$item" ] && continue if [ -z "$out" ]; then out="$item" else out="$out, $item" fi done printf '%s' "$out" } write_config() { LOG_LEVEL="${LOG_LEVEL:-error}" # SOCKS inbound tag for WARP. Kept as "mixed-in" in WARP-only mode for # backwards compatibility, "warp-in" when Proton is present too. if [ "$ENABLE_PROTON" = "1" ]; then WARP_IN_TAG="warp-in" else WARP_IN_TAG="mixed-in" fi # Default outbound for everything that is not a hard SOCKS rule # (TUN rules, routing lists, IP-leak sites, proxy DNS). # With both endpoints the user's choice wins; otherwise the only # available endpoint becomes the default. if [ "$ENABLE_WARP" = "1" ] && [ "$ENABLE_PROTON" = "1" ]; then if [ "$DEFAULT_ROUTE" = "proton" ]; then DEFAULT_OUTBOUND="proton-out" else DEFAULT_OUTBOUND="warp-out" fi elif [ "$ENABLE_PROTON" = "1" ]; then DEFAULT_OUTBOUND="proton-out" else DEFAULT_OUTBOUND="warp-out" fi echo "Default route outbound: $DEFAULT_OUTBOUND" >&2 if [ "$ENABLE_TUN" = "1" ]; then EXCLUDE_IFACE=$(detect_default_interface) || { echo "Warning: could not detect default network interface, using eth0" >&2 EXCLUDE_IFACE="eth0" } echo "TUN enabled, exclude_interface: $EXCLUDE_IFACE" >&2 TUN_INBOUND_BODY=$(cat <&2 TUN_INBOUND_BODY="" TUN_ROUTE_RULE_PART="" RULE_SET_RULE_PART="" ROUTE_RULE_SET_SECTION="" fi # --- AmneziaWG i1/i2 lines (WARP only) --- if [ -n "$I1" ] || [ -n "$I2" ]; then H4_COMMA="," else H4_COMMA="" fi if [ -n "$I1" ] && [ -n "$I2" ]; then I1_LINE=$(printf ' "i1": "%s",\n' "$I1") I2_LINE=$(printf ' "i2": "%s"\n' "$I2") elif [ -n "$I1" ]; then I1_LINE=$(printf ' "i1": "%s"\n' "$I1") I2_LINE="" elif [ -n "$I2" ]; then I1_LINE="" I2_LINE=$(printf ' "i2": "%s"\n' "$I2") else I1_LINE="" I2_LINE="" fi # --- WARP endpoint / inbound / route rule --- if [ "$ENABLE_WARP" = "1" ]; then echo "WARP enabled, adding $TAG endpoint and $WARP_IN_TAG inbound (:2080)" >&2 WARP_ENDPOINT_BODY=$(cat <&2 WARP_ENDPOINT_BODY="" WARP_INBOUND_BODY="" WARP_ROUTE_RULE="" fi # --- Proton endpoint / inbound / route rule --- if [ "$ENABLE_PROTON" = "1" ]; then echo "Proton enabled, adding proton-out endpoint and proton-in inbound (:3080)" >&2 PROTON_ENDPOINT_BODY=$(cat <&2 PROTON_ENDPOINT_BODY="" PROTON_INBOUND_BODY="" PROTON_ROUTE_RULE="" fi # --- Assemble dynamic arrays --- ENDPOINTS_BODY=$(join_objects "$WARP_ENDPOINT_BODY" "$PROTON_ENDPOINT_BODY") INBOUNDS_BODY=$(join_objects "$WARP_INBOUND_BODY" "$PROTON_INBOUND_BODY" "$TUN_INBOUND_BODY") SOCKS_ROUTE_RULES=$(join_objects "$WARP_ROUTE_RULE" "$PROTON_ROUTE_RULE") cat > "$OUTPUT_CONFIG" <