From bf2390c6ba22abdff5bcb59a30702786bb9078b2 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 20 Jul 2026 08:35:22 +0800 Subject: [PATCH] feat: add optional UDP relay support --- README.md | 3 ++ ss-rust.sh | 85 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 8618847..6f92810 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 一键安装 shadowsocks-rust,自动配置 ss2022 双节点,生成订阅链接 + Surge/Clash 配置。 +安装时可选择是否启用 UDP(默认启用)。普通节点会使用 TCP+UDP;HTTP obfs 节点保留 obfs TCP 前端,并在相同公网端口额外提供原生 Shadowsocks UDP。 + ## 一键安装 ```bash @@ -15,6 +17,7 @@ curl -sL https://mjjtop.com/ss -o ss-rust.sh && chmod +x ss-rust.sh && ./ss-rust - 🎲 随机端口 + 随机密钥 - ⏰ 自动 NTP 时间同步 - 📋 自动生成 SS 订阅链接 / Surge 配置 / Clash 配置 +- 📡 可控 UDP 转发,生成的 Clash 节点会同步标记 UDP 能力 - 🔄 systemd 管理,开机自启 ## 输出文件 diff --git a/ss-rust.sh b/ss-rust.sh index e0133f6..67669e6 100644 --- a/ss-rust.sh +++ b/ss-rust.sh @@ -29,6 +29,7 @@ METHOD_OBFS="" OBFS_BACKEND_PORT="" OBFS_HOST="" NODE_MODE="" +ENABLE_UDP="1" # ============ 基础检测 ============ check_root() { @@ -228,6 +229,7 @@ select_and_configure() { PORT_2022="" ; KEY_2022="" ; METHOD_2022="" PORT_RAW="" ; KEY_RAW="" ; METHOD_RAW="" PORT_OBFS="" ; KEY_OBFS="" ; METHOD_OBFS="" ; OBFS_BACKEND_PORT="" ; OBFS_HOST="" + ENABLE_UDP="1" echo "" echo -e "${CYAN}════════════════════════════════════════${NC}" @@ -242,6 +244,14 @@ select_and_configure() { read -rp "请选择 [1-4] (默认3): " node_choice node_choice=${node_choice:-3} + read -rp "启用 UDP 转发? [Y/n]: " udp_choice + udp_choice=${udp_choice:-Y} + if [[ "$udp_choice" =~ ^[Yy]$ ]]; then + ENABLE_UDP="1" + else + ENABLE_UDP="0" + fi + if [[ "$node_choice" == "1" || "$node_choice" == "3" ]]; then local dp=$((RANDOM % 10000 + 20000)) local dk=$(openssl rand -base64 16) @@ -288,6 +298,8 @@ select_and_configure() { python3 -c " import json servers = [] +udp_enabled = '${ENABLE_UDP}' == '1' +direct_mode = 'tcp_and_udp' if udp_enabled else 'tcp_only' if '${PORT_2022}': servers.append({ 'server': '0.0.0.0', @@ -295,7 +307,8 @@ if '${PORT_2022}': 'method': '${METHOD_2022}', 'password': '${KEY_2022}', 'timeout': 300, - 'fast_open': True + 'fast_open': True, + 'mode': direct_mode }) if '${PORT_RAW}': servers.append({ @@ -304,17 +317,30 @@ if '${PORT_RAW}': 'method': '${METHOD_RAW}', 'password': '${KEY_RAW}', 'timeout': 300, - 'fast_open': True + 'fast_open': True, + 'mode': direct_mode }) if '${PORT_OBFS}': + # simple-obfs only fronts TCP. Keep the obfuscated TCP backend on + # localhost and expose native Shadowsocks UDP on the public port. servers.append({ 'server': '127.0.0.1', 'server_port': int('${OBFS_BACKEND_PORT}'), 'method': '${METHOD_OBFS}', 'password': '${KEY_OBFS}', 'timeout': 300, - 'fast_open': True + 'fast_open': True, + 'mode': 'tcp_only' }) + if udp_enabled: + servers.append({ + 'server': '0.0.0.0', + 'server_port': int('${PORT_OBFS}'), + 'method': '${METHOD_OBFS}', + 'password': '${KEY_OBFS}', + 'timeout': 300, + 'mode': 'udp_only' + }) with open('/etc/shadowsocks-rust/config.json', 'w') as f: json.dump({'servers': servers}, f, indent=4) print('OK') @@ -327,6 +353,7 @@ KEY_OBFS=${KEY_OBFS} METHOD_OBFS=${METHOD_OBFS} OBFS_BACKEND_PORT=${OBFS_BACKEND_PORT} OBFS_HOST=${OBFS_HOST} +ENABLE_UDP=${ENABLE_UDP} EOF else rm -f /etc/shadowsocks-rust/obfs.env @@ -415,12 +442,28 @@ load_config() { PORT_2022="" ; KEY_2022="" ; METHOD_2022="" PORT_RAW="" ; KEY_RAW="" ; METHOD_RAW="" PORT_OBFS="" ; KEY_OBFS="" ; METHOD_OBFS="" ; OBFS_BACKEND_PORT="" ; OBFS_HOST="" + ENABLE_UDP="0" - eval $(python3 -c " -import json + if [[ -f /etc/shadowsocks-rust/obfs.env ]]; then + # shellcheck disable=SC1091 + source /etc/shadowsocks-rust/obfs.env + fi + + eval $(OBFS_BACKEND_PORT="${OBFS_BACKEND_PORT}" PORT_OBFS="${PORT_OBFS}" python3 -c " +import json, os with open('/etc/shadowsocks-rust/config.json') as f: c = json.load(f) +skip_ports = { + int(p) for p in (os.environ.get('OBFS_BACKEND_PORT'), os.environ.get('PORT_OBFS')) + if p and p.isdigit() +} +udp_enabled = False for s in c['servers']: + mode = s.get('mode', 'tcp_only') + if mode in ('tcp_and_udp', 'udp_only'): + udp_enabled = True + if s.get('server_port') in skip_ports or mode == 'udp_only': + continue m = s['method'] if '2022' in m: print(f'PORT_2022={s[\"server_port\"]}') @@ -430,14 +473,8 @@ for s in c['servers']: print(f'PORT_RAW={s[\"server_port\"]}') print(f'KEY_RAW={s[\"password\"]}') print(f'METHOD_RAW={m}') +print(f'ENABLE_UDP={1 if udp_enabled else 0}') " 2>/dev/null) - if [[ -f /etc/shadowsocks-rust/obfs.env ]]; then - # shellcheck disable=SC1091 - source /etc/shadowsocks-rust/obfs.env - if [[ -n "$OBFS_BACKEND_PORT" && "$PORT_2022" == "$OBFS_BACKEND_PORT" ]]; then - PORT_2022="" ; KEY_2022="" ; METHOD_2022="" - fi - fi return 0 } @@ -448,14 +485,19 @@ gen_subscribe() { mkdir -p "$sub_dir" local uris="" surge="" clash="" info_txt="" + local udp_clash="false" udp_label="关闭" + if [[ "${ENABLE_UDP:-0}" == "1" ]]; then + udp_clash="true" + udp_label="开启" + fi if [[ -n "$PORT_2022" ]]; then local uri="ss://$(echo -n "${METHOD_2022}:${KEY_2022}" | base64 -w0)@${SERVER_IP}:${PORT_2022}#SS2022-128" URI_2022="$uri" uris="${uris}${uri}\n" surge="${surge}SS2022-128 = ss, ${SERVER_IP}, ${PORT_2022}, encrypt-method=${METHOD_2022}, password=${KEY_2022}\n" - clash="${clash} - name: SS2022-128\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_2022}\n cipher: ${METHOD_2022}\n password: \"${KEY_2022}\"\n\n" - info_txt="${info_txt}【SS2022-AES-128】新协议\n 地址: ${SERVER_IP}\n 端口: ${PORT_2022}\n 加密: ${METHOD_2022}\n 密码: ${KEY_2022}\n\n" + clash="${clash} - name: SS2022-128\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_2022}\n cipher: ${METHOD_2022}\n password: \"${KEY_2022}\"\n udp: ${udp_clash}\n\n" + info_txt="${info_txt}【SS2022-AES-128】新协议\n 地址: ${SERVER_IP}\n 端口: ${PORT_2022}\n 加密: ${METHOD_2022}\n 密码: ${KEY_2022}\n UDP: ${udp_label}\n\n" fi if [[ -n "$PORT_RAW" ]]; then @@ -463,8 +505,8 @@ gen_subscribe() { URI_RAW="$uri" uris="${uris}${uri}\n" surge="${surge}SS-AES-128 = ss, ${SERVER_IP}, ${PORT_RAW}, encrypt-method=${METHOD_RAW}, password=${KEY_RAW}\n" - clash="${clash} - name: SS-AES-128\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_RAW}\n cipher: ${METHOD_RAW}\n password: \"${KEY_RAW}\"\n\n" - info_txt="${info_txt}【SS-AES-128】传统协议\n 地址: ${SERVER_IP}\n 端口: ${PORT_RAW}\n 加密: ${METHOD_RAW}\n 密码: ${KEY_RAW}\n\n" + clash="${clash} - name: SS-AES-128\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_RAW}\n cipher: ${METHOD_RAW}\n password: \"${KEY_RAW}\"\n udp: ${udp_clash}\n\n" + info_txt="${info_txt}【SS-AES-128】传统协议\n 地址: ${SERVER_IP}\n 端口: ${PORT_RAW}\n 加密: ${METHOD_RAW}\n 密码: ${KEY_RAW}\n UDP: ${udp_label}\n\n" fi if [[ -n "$PORT_OBFS" ]]; then @@ -475,8 +517,8 @@ gen_subscribe() { URI_OBFS="$uri" uris="${uris}${uri}\n" surge="${surge}SS2022-OBFS-HTTP = ss, ${SERVER_IP}, ${PORT_OBFS}, encrypt-method=${METHOD_OBFS}, password=${KEY_OBFS}, obfs=http, obfs-host=${OBFS_HOST}\n" - clash="${clash} - name: SS2022-OBFS-HTTP\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_OBFS}\n cipher: ${METHOD_OBFS}\n password: \"${KEY_OBFS}\"\n udp: false\n plugin: obfs\n plugin-opts:\n mode: http\n host: ${OBFS_HOST}\n\n" - info_txt="${info_txt}【SS2022 + HTTP obfs】simple-obfs 伪装\n 地址: ${SERVER_IP}\n 端口: ${PORT_OBFS}\n 加密: ${METHOD_OBFS}\n 密码: ${KEY_OBFS}\n 插件: obfs/http\n Host: ${OBFS_HOST}\n 后端: 127.0.0.1:${OBFS_BACKEND_PORT}\n\n" + clash="${clash} - name: SS2022-OBFS-HTTP\n type: ss\n server: ${SERVER_IP}\n port: ${PORT_OBFS}\n cipher: ${METHOD_OBFS}\n password: \"${KEY_OBFS}\"\n udp: ${udp_clash}\n plugin: obfs\n plugin-opts:\n mode: http\n host: ${OBFS_HOST}\n\n" + info_txt="${info_txt}【SS2022 + HTTP obfs】simple-obfs 伪装\n 地址: ${SERVER_IP}\n 端口: ${PORT_OBFS}\n 加密: ${METHOD_OBFS}\n 密码: ${KEY_OBFS}\n 插件: obfs/http\n Host: ${OBFS_HOST}\n 后端: 127.0.0.1:${OBFS_BACKEND_PORT}\n UDP: ${udp_label}(公网原生 SS UDP)\n\n" fi echo -e "$uris" | base64 -w0 > "$sub_dir/subscribe.txt" @@ -489,6 +531,8 @@ gen_subscribe() { # ============ 显示结果 ============ show_result() { load_config || return + local udp_label="关闭" + [[ "${ENABLE_UDP:-0}" == "1" ]] && udp_label="开启" echo "" echo -e "${CYAN}════════════════════════════════════════${NC}" echo -e "${CYAN} 🚀 Shadowsocks-Rust 安装完成${NC}" @@ -501,6 +545,7 @@ show_result() { echo -e " 端口: ${YELLOW}${PORT_2022}${NC}" echo -e " 加密: ${METHOD_2022}" echo -e " 密码: ${YELLOW}${KEY_2022}${NC}" + echo -e " UDP: ${udp_label}" echo "" fi @@ -510,6 +555,7 @@ show_result() { echo -e " 端口: ${YELLOW}${PORT_RAW}${NC}" echo -e " 加密: ${METHOD_RAW}" echo -e " 密码: ${YELLOW}${KEY_RAW}${NC}" + echo -e " UDP: ${udp_label}" echo "" fi @@ -521,6 +567,7 @@ show_result() { echo -e " 密码: ${YELLOW}${KEY_OBFS}${NC}" echo -e " 插件: obfs / http" echo -e " Host: ${OBFS_HOST}" + echo -e " UDP: ${udp_label}" echo "" fi @@ -607,7 +654,7 @@ import json with open('/etc/shadowsocks-rust/config.json') as f: c = json.load(f) for s in c.get('servers', []): - if str(s.get('server_port')) == '${OBFS_BACKEND_PORT}': + if str(s.get('server_port')) in ('${OBFS_BACKEND_PORT}', '${PORT_OBFS}'): s['password'] = '${KEY_OBFS}' with open('/etc/shadowsocks-rust/config.json','w') as f: json.dump(c, f, indent=4)