#!/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()