Skip to content

Commit 552304b

Browse files
committed
feat: add uuid seed option for deterministic UUID/password generation in relay mode
1 parent 49e0527 commit 552304b

2 files changed

Lines changed: 83 additions & 7 deletions

File tree

singbox2proxy/base.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,7 @@ def __init__(
14711471
relay_protocol: str = None,
14721472
relay_host: str = None,
14731473
relay_port: int = None,
1474+
uuid_seed: str = None,
14741475
):
14751476
"""Initialize a SingBoxProxy instance.
14761477
@@ -1508,6 +1509,7 @@ def __init__(
15081509
(e.g., "vmess", "ss", "trojan").
15091510
relay_host: Hostname or IP address of the relay server.
15101511
relay_port: Port of the relay server. If None, an unused port is automatically selected
1512+
uuid_seed: Optional seed string for generating consistent UUIDs in relay mode.
15111513
15121514
Raises:
15131515
TypeError: If config is not a string or path-like object.
@@ -1571,6 +1573,7 @@ def __init__(
15711573
self.relay_host = relay_host
15721574
self.relay_port = relay_port or (self._pick_unused_port([self.http_port, self.socks_port]) if relay_protocol else None)
15731575
self.relay_url = None
1576+
self.uuid_seed = uuid_seed
15741577
self._relay_credentials = {} # Store credentials for URL generation
15751578

15761579
# Runtime state
@@ -1704,6 +1707,40 @@ def stderr(self) -> str:
17041707
"""
17051708
return "".join(self._stderr_lines)
17061709

1710+
def _generate_deterministic_uuid(self, seed: str, suffix: str = "") -> str:
1711+
"""Generate a deterministic UUID from a seed string.
1712+
1713+
Args:
1714+
seed: The seed string to generate UUID from
1715+
suffix: Optional suffix to append to seed for different UUIDs
1716+
1717+
Returns:
1718+
str: A valid UUID v5 string
1719+
"""
1720+
import uuid
1721+
1722+
# Use UUID5 with a namespace for deterministic generation
1723+
namespace = uuid.NAMESPACE_DNS
1724+
combined_seed = f"{seed}{suffix}"
1725+
return str(uuid.uuid5(namespace, combined_seed))
1726+
1727+
def _generate_deterministic_password(self, seed: str, length: int = 16) -> str:
1728+
"""Generate a deterministic password from a seed string.
1729+
1730+
Args:
1731+
seed: The seed string to generate password from
1732+
length: Length of the password (for shadowsocks compatibility)
1733+
1734+
Returns:
1735+
str: A deterministic password
1736+
"""
1737+
import hashlib
1738+
1739+
# Generate deterministic password from seed
1740+
hash_obj = hashlib.sha256(seed.encode())
1741+
# Take hex digest and truncate to desired length
1742+
return hash_obj.hexdigest()[:length]
1743+
17071744
def _get_public_ip(self) -> str:
17081745
"""Get the public IP address of this machine.
17091746
@@ -1813,7 +1850,10 @@ def _generate_relay_inbound(self) -> dict:
18131850
port = self.relay_port
18141851

18151852
if protocol == "vmess":
1816-
user_id = str(uuid.uuid4())
1853+
if self.uuid_seed:
1854+
user_id = self._generate_deterministic_uuid(self.uuid_seed, "vmess")
1855+
else:
1856+
user_id = str(uuid.uuid4())
18171857
self._relay_credentials["uuid"] = user_id
18181858
return {
18191859
"type": "vmess",
@@ -1824,12 +1864,18 @@ def _generate_relay_inbound(self) -> dict:
18241864
}
18251865

18261866
elif protocol == "trojan":
1827-
password = str(uuid.uuid4())
1867+
if self.uuid_seed:
1868+
password = self._generate_deterministic_password(self.uuid_seed + "trojan", 36)
1869+
else:
1870+
password = str(uuid.uuid4())
18281871
self._relay_credentials["password"] = password
18291872
return {"type": "trojan", "tag": "relay-in", "listen": "0.0.0.0", "listen_port": port, "users": [{"password": password}]}
18301873

18311874
elif protocol in ("ss", "shadowsocks"):
1832-
password = str(uuid.uuid4())[:16]
1875+
if self.uuid_seed:
1876+
password = self._generate_deterministic_password(self.uuid_seed + "shadowsocks", 16)
1877+
else:
1878+
password = str(uuid.uuid4())[:16]
18331879
self._relay_credentials["password"] = password
18341880
return {
18351881
"type": "shadowsocks",

singbox2proxy/cli.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,15 @@ def main():
8282
parser.add_argument(
8383
"--relay",
8484
choices=["vmess", "trojan", "ss", "shadowsocks", "socks", "http"],
85-
help="Create a shareable proxy URL that relays traffic (e.g., --relay vless). Can be used without a proxy URL for direct connection.",
85+
help="Create a shareable proxy URL that relays traffic (e.g., --relay vmess). Can be used without a proxy URL for direct connection.",
8686
)
8787

8888
parser.add_argument("--relay-host", help="Host/IP to use in the relay URL (default: auto-detect)")
8989

9090
parser.add_argument("--relay-port", type=int, help="Port to use for the relay server (default: auto-assign)")
9191

92+
parser.add_argument("--uuid-seed", help="Seed for deterministic UUID/password generation (makes relay URLs persistent across restarts)")
93+
9294
args = parser.parse_args()
9395

9496
# Configure logging
@@ -190,6 +192,7 @@ def main():
190192
relay_protocol=args.relay,
191193
relay_host=args.relay_host,
192194
relay_port=args.relay_port,
195+
uuid_seed=args.uuid_seed,
193196
)
194197
proxies.append(main_proxy)
195198

@@ -232,15 +235,42 @@ def _save_config(config, path):
232235
# Test the proxy if requested
233236
if args.test:
234237
print("\nTesting proxy connection...")
238+
239+
# Ping test
240+
print("\n1. Ping Test:")
241+
try:
242+
import time as time_module
243+
ping_times = []
244+
for i in range(3):
245+
start = time_module.time()
246+
response = main_proxy.request("GET", "https://www.google.com/generate_204", timeout=5)
247+
elapsed = (time_module.time() - start) * 1000 # Convert to ms
248+
if response.status_code in (200, 204):
249+
ping_times.append(elapsed)
250+
print(f" Attempt {i+1}: {elapsed:.2f} ms")
251+
else:
252+
print(f" Attempt {i+1}: Failed (status {response.status_code})")
253+
254+
if ping_times:
255+
avg_ping = sum(ping_times) / len(ping_times)
256+
min_ping = min(ping_times)
257+
max_ping = max(ping_times)
258+
print(f" Average: {avg_ping:.2f} ms (min: {min_ping:.2f} ms, max: {max_ping:.2f} ms)")
259+
except Exception as e:
260+
print(f" Ping test failed: {str(e)}")
261+
262+
# IP test
263+
print("\n2. IP Detection Test:")
235264
try:
236265
response = main_proxy.request("GET", "https://api.ipify.org?format=json")
237266
if response.status_code == 200:
238267
ip_data = response.json()
239-
print(f"Proxy test successful! External IP: {ip_data['ip']}")
268+
print(f" External IP: {ip_data['ip']}")
269+
print("\n✓ All tests passed!")
240270
else:
241-
print(f"Proxy test failed with status code: {response.status_code}")
271+
print(f" Failed with status code: {response.status_code}")
242272
except Exception as e:
243-
print(f"Proxy test failed: {str(e)}")
273+
print(f" IP test failed: {str(e)}")
244274
sys.exit(0)
245275

246276
print("\nProxy is running. Press Ctrl+C to stop.")

0 commit comments

Comments
 (0)