Files
proxy-station/.claude/skills/edge-node-multi-protocol/scripts/16-add-socks5.py
YANG JIANKUAN 5326c572a2 feat: 合并边缘节点运维并完善 Surge 配置安全
迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。

完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。

验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
2026-10-03 23:19:59 +08:00

84 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Runs ON the box. Adds a password-authenticated SOCKS5 inbound to 3x-ui.
Reuses the existing Xray instance -- no new daemon. Xray implements SOCKS5 with
RFC 1929 username/password auth natively, so this is purely a new inbound row.
Additive and idempotent: skips if the port already exists, touches no other
inbound, writes nothing to nginx or sing-box.
Two things worth knowing about this inbound versus the other ten on this node:
* It is NOT wrapped in TLS. SOCKS5 auth per RFC 1929 sends the username and
password in CLEARTEXT, and the tunnelled traffic is unencrypted too. Anyone
on the path can read both. Raw SOCKS5 is also trivially fingerprinted. Treat
the credentials as disposable and prefer binding to 127.0.0.1 + an SSH tunnel
if the client can be pointed at a local port (see LISTEN below).
* `socks` is not one of the protocols 3x-ui projects through its clients /
client_inbounds tables (that set is vmess/vless/trojan/shadowsocks/hysteria),
so the accounts live directly in this inbound's settings JSON and the
settings.clients migration hazard documented in 08-xui-bootstrap.py fix #5
does not apply here.
Env in: SOCKS_PORT, SOCKS_USER, SOCKS_PASS, SERVER_IP
LISTEN (default 0.0.0.0; set 127.0.0.1 for SSH-tunnel-only access)
REMARK (default socks5-direct), XUI_DB
"""
import json, os, sqlite3, subprocess, sys, time
DB = os.environ.get("XUI_DB", "/etc/x-ui/x-ui.db")
PORT = int(os.environ["SOCKS_PORT"])
USER = os.environ["SOCKS_USER"]
PASS = os.environ["SOCKS_PASS"]
LISTEN = os.environ.get("LISTEN", "0.0.0.0")
REMARK = os.environ.get("REMARK", "socks5-direct")
# Xray announces this address in the UDP ASSOCIATE reply. For a remote server it
# must be the public IP, or UDP-over-SOCKS5 breaks (TCP still works either way).
SERVER_IP = os.environ.get("SERVER_IP", "127.0.0.1")
TAG = "inbound-" + REMARK
def cols(cur, table):
return {r[1] for r in cur.execute("PRAGMA table_info(%s)" % table).fetchall()}
def main():
con = sqlite3.connect(DB)
cur = con.cursor()
if cur.execute("SELECT 1 FROM inbounds WHERE port=?", (PORT,)).fetchone():
print(" inbound :%d already exists, nothing to do" % PORT)
return
row = {
"user_id": 1, # NULL user_id -> proxies fine but INVISIBLE in the panel
"up": 0, "down": 0, "total": 0, "remark": REMARK,
"enable": 1, "expiry_time": 0,
"listen": LISTEN, "port": PORT, "protocol": "socks",
"settings": json.dumps({
"auth": "password",
"accounts": [{"user": USER, "pass": PASS}],
"udp": True,
"ip": SERVER_IP,
}, indent=2),
"stream_settings": json.dumps({"network": "tcp", "security": "none"}, indent=2),
"sniffing": json.dumps({"enabled": True, "destOverride": ["http", "tls"]}),
"tag": TAG,
}
ic = cols(cur, "inbounds")
keys = [k for k in row if k in ic]
cur.execute("INSERT INTO inbounds (%s) VALUES (%s)" % (",".join(keys), ",".join("?" * len(keys))),
[row[k] for k in keys])
con.commit()
con.close()
print(" inbound :%d socks5 added (listen=%s, auth=password, udp=on)" % (PORT, LISTEN))
subprocess.run(["systemctl", "restart", "x-ui"], check=True)
time.sleep(6)
print(" x-ui restarted")
if __name__ == "__main__":
main()