diff --git a/README.md b/README.md index 845b7ec..da107e2 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,8 @@ Server options: --db FILE SQLite database path (default: ~/.simtrace-analyser/sessions.db) --capture MODE gsmtap | direct | disabled (default: gsmtap) --gsmtap-port PORT UDP port for the GSMTAP listener (default: 4729) +--gsmtap-bind ADDR UDP bind address for the GSMTAP listener (default: 127.0.0.1; + use 0.0.0.0 to receive from a Docker bridge or another host) --web-dir PATH PWA static files directory (default: the PWA bundled in the installed package, or /frontend from source) ``` diff --git a/README_RUS.md b/README_RUS.md index 96e79d5..721b351 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -351,6 +351,8 @@ TS 51.011), поэтому FID, повторно используемый в р --db FILE путь к базе SQLite (по умолчанию: ~/.simtrace-analyser/sessions.db) --capture MODE gsmtap | direct | disabled (по умолчанию: gsmtap) --gsmtap-port PORT UDP-порт листенера GSMTAP (по умолчанию: 4729) +--gsmtap-bind ADDR UDP-адрес привязки листенера GSMTAP (по умолчанию: 127.0.0.1; + используйте 0.0.0.0, чтобы принимать из Docker-сети или с другого хоста) --web-dir PATH каталог статических файлов PWA (по умолчанию: PWA внутри установленного пакета или /frontend при запуске из исходников) ``` diff --git a/simtrace_analyser/__main__.py b/simtrace_analyser/__main__.py index b0eeee6..527a4e5 100644 --- a/simtrace_analyser/__main__.py +++ b/simtrace_analyser/__main__.py @@ -1,7 +1,8 @@ """simtrace-analyser-server — CLI entry point. Usage: - simtrace-analyser-server --capture gsmtap [--gsmtap-port 4729] [--port 8081] + simtrace-analyser-server --capture gsmtap [--gsmtap-bind 0.0.0.0] + [--gsmtap-port 4729] [--port 8081] simtrace-analyser-server --capture direct [--port 8081] """ @@ -46,6 +47,9 @@ def main(): help='Capture mode: gsmtap (listen UDP 4729), direct (SIMtrace2 USB), or disabled (no capture)') p.add_argument('--gsmtap-port', type=int, default=4729, help='UDP port for GSMTAP listener (default: 4729)') + p.add_argument('--gsmtap-bind', default='127.0.0.1', metavar='ADDR', + help='UDP bind address for the GSMTAP listener ' + '(default: 127.0.0.1; use 0.0.0.0 in a container)') p.add_argument('--log-interval', type=float, default=60.0, help='Seconds between capture liveness heartbeat logs ' '(default: 60.0)') @@ -61,7 +65,7 @@ def main(): capture = None if args.capture == 'gsmtap': - backend = GsmtapListener(bind_port=args.gsmtap_port) + backend = GsmtapListener(bind_ip=args.gsmtap_bind, bind_port=args.gsmtap_port) capture = CaptureManager(backend, db, log_interval=args.log_interval) elif args.capture == 'direct': backend = DirectSniffer() @@ -78,6 +82,7 @@ def main(): print(f'simtrace-analyser-server v{__version__} — http://{args.host}:{args.port}', file=sys.stderr) print(f' capture mode: {args.capture}', file=sys.stderr) if args.capture == 'gsmtap': + print(f' GSMTAP bind: {args.gsmtap_bind}', file=sys.stderr) print(f' GSMTAP port: {args.gsmtap_port}', file=sys.stderr) print(f' database: {args.db}', file=sys.stderr) print(f' PWA served at http://{args.host}:{args.port}/', file=sys.stderr) diff --git a/simtrace_analyser/capture.py b/simtrace_analyser/capture.py index 740f0ec..c87d3f5 100644 --- a/simtrace_analyser/capture.py +++ b/simtrace_analyser/capture.py @@ -39,15 +39,17 @@ def gsmtap_msg_type(sub_type): class GsmtapListener: - def __init__(self, bind_port=4729): + def __init__(self, bind_port=4729, bind_ip='127.0.0.1'): self._bind_port = bind_port - self._receiver = GsmtapReceiver(bind_port=bind_port) + self._bind_ip = bind_ip + self._receiver = GsmtapReceiver(bind_ip=bind_ip, bind_port=bind_port) self._running = False self._dropped = 0 def start(self): if getattr(self._receiver, '_closed', False): - self._receiver = GsmtapReceiver(bind_port=self._bind_port) + self._receiver = GsmtapReceiver(bind_ip=self._bind_ip, + bind_port=self._bind_port) self._running = True self._dropped = 0 diff --git a/tests/test_capture.py b/tests/test_capture.py index d6428e8..6a94f55 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -206,6 +206,41 @@ class TestGsmtapListenerRobustness(unittest.TestCase): self.assertEqual(listener.dropped, 1) +class TestGsmtapListenerBind(unittest.TestCase): + def test_bind_ip_reaches_the_receiver(self): + from simtrace_analyser import capture + calls = [] + + class _FakeReceiver: + def __init__(self, bind_ip='127.0.0.1', bind_port=4729): + calls.append((bind_ip, bind_port)) + + def close(self): + pass + + real = capture.GsmtapReceiver + capture.GsmtapReceiver = _FakeReceiver + try: + listener = capture.GsmtapListener(bind_ip='0.0.0.0', bind_port=4729) + listener._receiver._closed = True # force start() to re-create it + listener.start() + finally: + capture.GsmtapReceiver = real + self.assertEqual(calls, [('0.0.0.0', 4729), ('0.0.0.0', 4729)]) + + def test_all_interfaces_bind(self): + from simtrace_analyser.capture import GsmtapListener + listener = GsmtapListener(bind_ip='0.0.0.0', bind_port=0) + self.addCleanup(listener.stop) + self.assertEqual(listener._receiver._sock.getsockname()[0], '0.0.0.0') + + def test_default_bind_is_loopback(self): + from simtrace_analyser.capture import GsmtapListener + listener = GsmtapListener(bind_port=0) + self.addCleanup(listener.stop) + self.assertEqual(listener._receiver._sock.getsockname()[0], '127.0.0.1') + + class TestCaptureLoopRobustness(unittest.TestCase): def test_capture_loop_survives_insert_error(self): mgr = CaptureManager(_OneShotBackend(), _NullDb())