mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-20 13:03:50 +00:00
Add `ss-local --printable-salt` / `"printable_salt": true`, disabled by default. For each outgoing TCP salt it picks a prefix length uniformly from 6-12 bytes and samples those bytes as printable ASCII (0x20-0x7e) immediately before key derivation. The salt stays 32 bytes with no framing or markers, so the wire format is unchanged and any stock server interoperates. Only chacha20-ietf-poly1305 and aes-256-gcm are accepted; other ciphers fail at startup. UDP, server responses, and AEAD-2022 are untouched. Crypto unit tests cover length distribution, empty first writes, exact framing overhead, salt-tail preservation, byte-at-a-time decryption, tampering and replay rejection. CLI tests check local-only exposure and cipher rejection via flag, config key and bad config type. interop.py gains --printable-salt and --server-bin for independent stock servers. docs/printable-salt.md records the design, entropy bound, fingerprint caveats and route-experiment procedure; docs/encoding-experiments.md and tests/experiments/ hold the Base85/lowpop85/TLS outer-transport prototypes. docs/measurements/printable-salt-2026-09-23/ archives the sanitized results of the aborted field trials. Co-authored-by: Claude <noreply@anthropic.com>
73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the real supervisor/backend and verify cleanup on lost control input."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
BINARY = None
|
|
|
|
|
|
class SupervisorTests(unittest.TestCase):
|
|
def check_cleanup(self, trigger):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
for name in ('route_server.py', 'transport.py'):
|
|
shutil.copy(Path(__file__).with_name(name), root / name)
|
|
env = dict(os.environ, SSH_CONNECTION='127.0.0.1 1 127.0.0.1 22')
|
|
with (root / 'log').open('w+') as log:
|
|
process = subprocess.Popen([sys.executable, str(root / 'route_server.py')],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log, text=True, env=env)
|
|
try:
|
|
process.stdin.write(json.dumps(dict(password='supervisor-test-only', ports=[],
|
|
capture=False, server_binary=str(BINARY), control_timeout=.3 if trigger == 'timeout' else 60)) + '\n')
|
|
process.stdin.flush()
|
|
import select
|
|
self.assertTrue(select.select([process.stdout], [], [], 5)[0], 'supervisor startup timeout')
|
|
ready = json.loads(process.stdout.readline())
|
|
self.assertTrue(ready['ready'])
|
|
for port in (ready['origin'], ready['backend']):
|
|
with socket.create_connection(('127.0.0.1', port), .5):
|
|
pass
|
|
if trigger == 'eof':
|
|
process.stdin.close()
|
|
elif trigger == 'signal':
|
|
process.send_signal(signal.SIGTERM)
|
|
process.wait(timeout=5)
|
|
if trigger == 'eof':
|
|
self.assertEqual(process.returncode, 0)
|
|
for port in (ready['origin'], ready['backend']):
|
|
with self.assertRaises(OSError):
|
|
socket.create_connection(('127.0.0.1', port), .2)
|
|
finally:
|
|
if process.poll() is None:
|
|
process.kill()
|
|
process.wait()
|
|
if not process.stdin.closed:
|
|
process.stdin.close()
|
|
process.stdout.close()
|
|
|
|
def test_control_eof(self):
|
|
self.check_cleanup('eof')
|
|
|
|
def test_control_silence(self):
|
|
self.check_cleanup('timeout')
|
|
|
|
def test_termination_signal(self):
|
|
self.check_cleanup('signal')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--bin', required=True)
|
|
args = parser.parse_args()
|
|
BINARY = Path(args.bin).resolve() / 'ss-server'
|
|
unittest.main(argv=[__file__])
|