#!/bin/bash set -e INSTALL_DIR="/opt/sing-box-warp" CONFIG_DIR="/etc/sing-box-warp" CACHE_DIR="/var/cache/sing-box-warp" SERVICE_NAME="sing-box-warp" SING_BOX_VERSION="1.13.2-extended-1.6.2" SING_BOX_URL="https://gitea.digitalforest.my/hesoyam/sing-box-extended-mirror/releases/download/v${SING_BOX_VERSION}/sing-box-${SING_BOX_VERSION}-linux-amd64.tar.gz" REINSTALL=0 is_existing_install() { if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then return 0 fi if systemctl list-unit-files "${SERVICE_NAME}.service" --no-legend 2>/dev/null | grep -q "${SERVICE_NAME}"; then return 0 fi if [ -d "$INSTALL_DIR" ] && [ -n "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then return 0 fi if [ -f "$CONFIG_DIR/warp.conf" ]; then return 0 fi return 1 } stop_existing_service() { local running=0 if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then running=1 echo "Service ${SERVICE_NAME} is running, stopping..." elif systemctl list-unit-files "${SERVICE_NAME}.service" --no-legend 2>/dev/null | grep -q "${SERVICE_NAME}"; then echo "Service ${SERVICE_NAME} is installed but not running, stopping..." fi systemctl stop "$SERVICE_NAME" 2>/dev/null || true systemctl disable "$SERVICE_NAME" 2>/dev/null || true if pgrep -f "sing-box run -c ${INSTALL_DIR}/config.json" >/dev/null 2>&1; then echo "Stopping remaining sing-box processes..." pkill -f "sing-box run -c ${INSTALL_DIR}/config.json" 2>/dev/null || true sleep 1 fi if [ "$running" -eq 1 ] || systemctl is-failed --quiet "$SERVICE_NAME" 2>/dev/null; then systemctl reset-failed "$SERVICE_NAME" 2>/dev/null || true fi } clean_existing_files() { echo "Removing previous installation files..." rm -rf "${INSTALL_DIR:?}"/* rm -f "$INSTALL_DIR/config.json" "$INSTALL_DIR/generate-config.sh" 2>/dev/null || true rm -rf "$INSTALL_DIR/rules" rm -f "$CONFIG_DIR/enable-tun" 2>/dev/null || true rm -f "$CONFIG_DIR/proton.conf" 2>/dev/null || true rm -f "$CONFIG_DIR/default-route" 2>/dev/null || true rm -rf "${CACHE_DIR:?}"/* } prepare_reinstall() { if ! is_existing_install; then return 0 fi REINSTALL=1 echo "" echo "=== Existing installation detected ===" stop_existing_service clean_existing_files systemctl daemon-reload 2>/dev/null || true echo "Ready for clean reinstall." echo "" } start_service() { echo "Generating config..." WARP_CONF="$CONFIG_DIR/warp.conf" \ PROTON_CONF="$CONFIG_DIR/proton.conf" \ OUTPUT_CONFIG="$INSTALL_DIR/config.json" \ ENABLE_TUN_FILE="$CONFIG_DIR/enable-tun" \ DEFAULT_ROUTE_FILE="$CONFIG_DIR/default-route" \ "$INSTALL_DIR/generate-config.sh" echo "Starting ${SERVICE_NAME}..." systemctl enable "$SERVICE_NAME" systemctl start "$SERVICE_NAME" sleep 2 systemctl status "$SERVICE_NAME" --no-pager || true } # Extract a JSON string field value (flat JSON): json_get "" "" json_get() { printf '%s' "$1" \ | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ | head -1 \ | sed -E "s/^\"$2\"[[:space:]]*:[[:space:]]*\"//; s/\"$//" } # Probe ip-api.com through a local SOCKS5 proxy on the given port. # Prints 8 values separated by 0x1F: latency, status, country, regionName, # city, timezone, isp, query. probe_values() { local port="$1" local sep=$'\037' local resp tsec body ms status country regionName city timezone isp query resp=$(curl -s --connect-timeout 8 --max-time 15 \ --proxy "socks5h://127.0.0.1:${port}" \ -w $'\n%{time_total}' "http://ip-api.com/json" 2>/dev/null) tsec=$(printf '%s' "$resp" | tail -n1) body=$(printf '%s' "$resp" | sed '$d') if [ -z "$body" ] || ! printf '%s' "$body" | grep -q '"status"'; then printf 'ERR%sno response%s-%s-%s-%s-%s-%s-' \ "$sep" "$sep" "$sep" "$sep" "$sep" "$sep" "$sep" return fi ms=$(awk "BEGIN{printf \"%d\", ${tsec:-0}*1000}") status=$(json_get "$body" status) country=$(json_get "$body" country) regionName=$(json_get "$body" regionName) city=$(json_get "$body" city) timezone=$(json_get "$body" timezone) isp=$(json_get "$body" isp) query=$(json_get "$body" query) printf '%sms%s%s%s%s%s%s%s%s%s%s%s%s%s%s' \ "$ms" "$sep" "$status" "$sep" "$country" "$sep" "$regionName" \ "$sep" "$city" "$sep" "$timezone" "$sep" "$isp" "$sep" "$query" } # Pad a string with trailing spaces to a given display width (char count). _pad() { local s="$1" w="$2" len=${#1} i printf '%s' "$s" i=$len while [ "$i" -lt "$w" ]; do printf ' '; i=$((i + 1)); done } _dashes() { local i=0 while [ "$i" -lt "$1" ]; do printf -- '-'; i=$((i + 1)); done } check_outbounds() { if ! command -v curl >/dev/null 2>&1; then echo "curl not found, cannot run checks." >&2 return 1 fi local labels=("ms до ответа" "status" "country" "regionName" "city" "timezone" "isp" "query") local n=${#labels[@]} local -a warp_col=() proton_col=() local have_warp="$ENABLE_WARP" have_proton="$ENABLE_PROTON" local w0=4 ww=4 wp=6 # widths of "поле" / "warp" / "proton" local i if [ "$have_warp" -eq 1 ]; then echo "Probing WARP (socks5://127.0.0.1:2080) ..." IFS=$'\037' read -r -a warp_col <<< "$(probe_values 2080)" fi if [ "$have_proton" -eq 1 ]; then echo "Probing Proton (socks5://127.0.0.1:3080) ..." IFS=$'\037' read -r -a proton_col <<< "$(probe_values 3080)" fi for ((i = 0; i < n; i++)); do [ ${#labels[i]} -gt "$w0" ] && w0=${#labels[i]} if [ "$have_warp" -eq 1 ]; then [ ${#warp_col[i]} -gt "$ww" ] && ww=${#warp_col[i]}; fi if [ "$have_proton" -eq 1 ]; then [ ${#proton_col[i]} -gt "$wp" ] && wp=${#proton_col[i]}; fi done _sep_line() { printf '+'; _dashes $((w0 + 2)); printf '+' [ "$have_warp" -eq 1 ] && { _dashes $((ww + 2)); printf '+'; } [ "$have_proton" -eq 1 ] && { _dashes $((wp + 2)); printf '+'; } printf '\n' } _row() { printf '| '; _pad "$1" "$w0"; printf ' |' [ "$have_warp" -eq 1 ] && { printf ' '; _pad "$2" "$ww"; printf ' |'; } [ "$have_proton" -eq 1 ] && { printf ' '; _pad "$3" "$wp"; printf ' |'; } printf '\n' } echo "" _sep_line _row "поле" "warp" "proton" _sep_line for ((i = 0; i < n; i++)); do _row "${labels[i]}" "${warp_col[i]:--}" "${proton_col[i]:--}" done _sep_line } echo "=== Sing-Box WARP Quick Installer ===" echo "" if [ "$EUID" -ne 0 ]; then echo "Please run as root (use sudo)" exit 1 fi echo "" echo "=== WARP Configuration (optional) ===" echo "goto ->> https://warp-generator.github.io/ generater for AWG 3.0" echo "WARP exposes a SOCKS5 proxy on port 2080." read -p "Add WARP configuration? (y/n) " -n 1 -r < /dev/tty echo ENABLE_WARP=0 WARP_INPUT="" if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Paste your WARP config (wg://... or [Interface]/[Peer] INI)." echo "Finish input with Ctrl-D." WARP_INPUT=$(cat < /dev/tty) if [ -z "$WARP_INPUT" ]; then echo "WARP input is empty, skipping WARP configuration." else ENABLE_WARP=1 fi else echo "Skipping WARP configuration." fi echo "" echo "=== Proton Configuration (optional) ===" echo "Proton (second WireGuard endpoint) exposes a SOCKS5 proxy on port 3080." read -p "Add Proton configuration? (y/n) " -n 1 -r < /dev/tty echo ENABLE_PROTON=0 PROTON_INPUT="" if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Paste your Proton config ([Interface]/[Peer] INI or wg://)." echo "Finish input with Ctrl-D." PROTON_INPUT=$(cat < /dev/tty) if [ -z "$PROTON_INPUT" ]; then echo "Proton input is empty, skipping Proton configuration." else ENABLE_PROTON=1 fi else echo "Skipping Proton configuration." fi if [ "$ENABLE_WARP" -eq 0 ] && [ "$ENABLE_PROTON" -eq 0 ]; then echo "" echo "Neither WARP nor Proton selected. Nothing to do, exiting." exit 0 fi prepare_reinstall echo "Creating directories..." mkdir -p "$INSTALL_DIR" mkdir -p "$CONFIG_DIR" mkdir -p "$CACHE_DIR" if [ "$ENABLE_WARP" -eq 1 ]; then printf "%s\n" "$WARP_INPUT" > "$CONFIG_DIR/warp.conf" echo "WARP configuration saved to $CONFIG_DIR/warp.conf" else rm -f "$CONFIG_DIR/warp.conf" fi if [ "$ENABLE_PROTON" -eq 1 ]; then printf "%s\n" "$PROTON_INPUT" > "$CONFIG_DIR/proton.conf" echo "Proton configuration saved to $CONFIG_DIR/proton.conf" else rm -f "$CONFIG_DIR/proton.conf" fi echo "" echo "=== TUN mode ===" echo "TUN routes system traffic through sing-box (needs CAP_NET_ADMIN)." echo "Without TUN, only the SOCKS5 proxies are available." read -p "Enable TUN mode? (y/n) " -n 1 -r < /dev/tty echo if [[ $REPLY =~ ^[Yy]$ ]]; then ENABLE_TUN=1 echo "1" > "$CONFIG_DIR/enable-tun" echo "TUN mode enabled." else ENABLE_TUN=0 echo "0" > "$CONFIG_DIR/enable-tun" echo "TUN mode disabled (SOCKS5 only)." fi if [ "$ENABLE_WARP" -eq 1 ] && [ "$ENABLE_PROTON" -eq 1 ]; then echo "" echo "=== Default route ===" echo "Choose where all non-SOCKS traffic goes (TUN rules, routing lists, IP-leak sites)." echo "SOCKS inbounds always keep their own outbound: 2080 -> WARP, 3080 -> Proton." echo " 1) WARP (warp-out)" echo " 2) Proton (proton-out)" read -p "Select default route [1/2] " -n 1 -r < /dev/tty echo if [[ $REPLY == "2" ]]; then DEFAULT_ROUTE="proton" echo "Default route: Proton (proton-out)." else DEFAULT_ROUTE="warp" echo "Default route: WARP (warp-out)." fi elif [ "$ENABLE_PROTON" -eq 1 ]; then DEFAULT_ROUTE="proton" echo "Default route: Proton (proton-out) — only endpoint." else DEFAULT_ROUTE="warp" echo "Default route: WARP (warp-out) — only endpoint." fi echo "$DEFAULT_ROUTE" > "$CONFIG_DIR/default-route" echo "" echo "Downloading sing-box..." NEED_DOWNLOAD=1 if [ "$REINSTALL" -eq 1 ]; then echo "Reinstall: updating sing-box binary..." NEED_DOWNLOAD=1 elif command -v sing-box >/dev/null 2>&1; then INSTALLED_VERSION=$(sing-box version 2>/dev/null | head -n 1 || true) if echo "$INSTALLED_VERSION" | grep -q "$SING_BOX_VERSION"; then NEED_DOWNLOAD=0 echo "sing-box already installed ($INSTALLED_VERSION), skipping download." else echo "sing-box is installed ($INSTALLED_VERSION) but version mismatch, downloading $SING_BOX_VERSION..." fi fi if [ "$NEED_DOWNLOAD" -eq 1 ]; then cd /tmp TARBALL="sing-box-${SING_BOX_VERSION}-linux-amd64.tar.gz" rm -f "$TARBALL" echo "Downloading $TARBALL ..." if ! wget -q --show-progress --timeout=20 --tries=3 --waitretry=5 --retry-connrefused --continue -O "$TARBALL" "$SING_BOX_URL"; then echo "wget failed, trying curl..." curl -fL --connect-timeout 20 --retry 3 --retry-delay 5 -o "$TARBALL" "$SING_BOX_URL" fi tar -xzf "$TARBALL" mv "sing-box-${SING_BOX_VERSION}-linux-amd64/sing-box" /usr/local/bin/sing-box chmod +x /usr/local/bin/sing-box rm -rf "$TARBALL" "sing-box-${SING_BOX_VERSION}-linux-amd64" fi GENERATE_CONFIG_URL="https://gitea.digitalforest.my/hesoyam/sing-warp-socks5/raw/branch/main/generate-config.sh" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo "Installing generate-config.sh..." if [ -f "$SCRIPT_DIR/generate-config.sh" ]; then cp "$SCRIPT_DIR/generate-config.sh" "$INSTALL_DIR/generate-config.sh" echo "Copied generate-config.sh from installer directory." elif wget -q --timeout=20 -O "$INSTALL_DIR/generate-config.sh" "$GENERATE_CONFIG_URL"; then echo "Downloaded generate-config.sh." elif curl -fsSL --connect-timeout 20 -o "$INSTALL_DIR/generate-config.sh" "$GENERATE_CONFIG_URL"; then echo "Downloaded generate-config.sh (curl)." else echo "Error: failed to install generate-config.sh" exit 1 fi chmod +x "$INSTALL_DIR/generate-config.sh" echo "Creating systemd service..." cat > /etc/systemd/system/sing-box-warp.service <<"'SERVICE_EOF'" [Unit] Description=Sing-Box WARP SOCKS5 Proxy After=network-online.target Wants=network-online.target [Service] Type=simple User=root WorkingDirectory=/opt/sing-box-warp Environment="HOME=/var/cache/sing-box-warp" Environment="WARP_CONF=/etc/sing-box-warp/warp.conf" Environment="PROTON_CONF=/etc/sing-box-warp/proton.conf" Environment="DEFAULT_ROUTE_FILE=/etc/sing-box-warp/default-route" Environment="OUTPUT_CONFIG=/opt/sing-box-warp/config.json" ExecStartPre=/opt/sing-box-warp/generate-config.sh ExecStart=/usr/local/bin/sing-box run -c /opt/sing-box-warp/config.json Restart=on-failure RestartSec=5s StandardOutput=journal StandardError=journal NoNewPrivileges=false PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/sing-box-warp /var/cache/sing-box-warp AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target 'SERVICE_EOF' echo "Configuring sysctl parameters..." cat > /etc/sysctl.d/99-sing-box-warp.conf <