scp81: rename passthru to redirect, add a true passthru mode (v2.2.13)

The former 'passthru' mode is now 'redirect': it pins the configured target
and every BIP channel is connected there (the card's requested address is
only logged; host/port required). The name 'passthru' now means the new mode:
no listener and no target - each channel dials the destination the card
requests in OPEN CHANNEL (Other address + Transport level port, TCP client,
remote, 0x02 only). The specs define no default port (TS 102 223 8.59), so an
incomplete or non-TCP request fails the channel with result 3A and an
open-fail log reason.

BipTerminal gains a mode (enable(host, port, mode=...), open(..., proto=...)),
reports it in status(), and the control API/status expose redirect (target)
and passthru (per-channel targets). The PWA mode selector shows four modes
with per-mode notes and disables Host/Port in passthru; the status line shows
each channel's actual target. Docs, help (EN/RU), READMEs and the AGENTS notes
updated; SW cache otaman-v179.

Breaking API change: mode:'passthru' has the new semantics - use
mode:'redirect' for the previous behavior (no alias).
This commit is contained in:
2026-09-17 08:17:28 +03:00
parent b02f28f6ce
commit 404fdf5a1c
13 changed files with 241 additions and 76 deletions
+35 -17
View File
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '2.2.12'
VERSION = '2.2.13'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -851,9 +851,12 @@ _PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES}
_BIP = httpota.BipTerminal()
_SCP81_LISTENER = None
# Active listener mode and target: ('dump'|'tls'|'passthru', host, port).
# passthru has no listener object - the BIP channels connect straight to the
# external platform - so the mode/target are tracked here for the status API.
# Active listener mode: 'dump' | 'tls' | 'redirect' | 'passthru'.
# 'redirect' pins one target and has no listener object - the BIP channels
# connect straight to the configured external platform (TLS terminated
# there). 'passthru' has neither listener nor target: each channel dials the
# destination the card requests in OPEN CHANNEL. Mode/target are tracked here
# for the status API.
_SCP81_MODE = None
_SCP81_TARGET = None
# PSK table of the TLS listener: identity -> key (memory only, never logged or
@@ -1409,7 +1412,7 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
# destination TLVs - see the AGENTS.md HTTP OTA notes).
_BIP.log('open-relaxed', address=addr, port=port,
note='destination/transport not fully specified')
cid, err = _BIP.open(addr or '-', port or 0, buffer_size)
cid, err = _BIP.open(addr or '-', port or 0, buffer_size, proto=proto)
if cid is None:
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00, extra)
if cmd_qual and (cmd_qual & 0x04):
@@ -1461,10 +1464,15 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
def _scp81_listener_status():
if not _SCP81_LISTENER:
if _SCP81_MODE == 'passthru' and _SCP81_TARGET:
return {'mode': 'passthru', 'host': _SCP81_TARGET[0],
if _SCP81_MODE == 'redirect' and _SCP81_TARGET:
return {'mode': 'redirect', 'host': _SCP81_TARGET[0],
'port': _SCP81_TARGET[1],
'target': '%s:%d' % _SCP81_TARGET}
if _SCP81_MODE == 'passthru':
# No listener and no pinned target: every channel dials the
# destination the card requests (per-channel targets in the
# BIP status).
return {'mode': 'passthru'}
return None
if isinstance(_SCP81_LISTENER, scp81.PskTlsServer):
return {'mode': 'tls', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port,
@@ -2010,18 +2018,28 @@ def _scp81_bip_control(body):
_SCP81_LISTENER.stop()
_SCP81_LISTENER = None
_BIP.disable()
if mode == 'passthru':
# No local listener: the card's BIP channels connect straight to the
# external platform (e.g. a production HTTP OTA server), which
# terminates TLS and runs the administration dialog. The target is a
# configured address, never the address the card requests.
if mode == 'redirect':
# No local listener: the card's BIP channels are redirected straight
# to the configured target (e.g. a production HTTP OTA server), which
# terminates TLS and runs the administration dialog. The address the
# card requests is only logged.
if not body.get('host') or body.get('port') in (None, ''):
return {'ok': False,
'error': 'passthru mode requires the target host and port'}
_SCP81_MODE = 'passthru'
'error': 'redirect mode requires the target host and port'}
_SCP81_MODE = 'redirect'
_SCP81_TARGET = (host, port)
_BIP.on_data = _bip_data_available
_BIP.enable(host, port)
_BIP.enable(host, port, mode='redirect')
return {'ok': True, 'bip': _BIP.status(),
'listener': _scp81_listener_status()}
if mode == 'passthru':
# No local listener and no pinned target: every BIP channel dials the
# destination the card requests in OPEN CHANNEL (Other address +
# Transport level port, TCP client only). Host and port are unused.
_SCP81_MODE = 'passthru'
_SCP81_TARGET = None
_BIP.on_data = _bip_data_available
_BIP.enable(mode='passthru')
return {'ok': True, 'bip': _BIP.status(),
'listener': _scp81_listener_status()}
if mode == 'tls':
@@ -2103,7 +2121,7 @@ def _scp81_bip_control(body):
_SCP81_MODE = 'tls'
_SCP81_TARGET = (_SCP81_LISTENER.host, _SCP81_LISTENER.port)
_BIP.on_data = _bip_data_available
_BIP.enable(host, _SCP81_LISTENER.port)
_BIP.enable(host, _SCP81_LISTENER.port, mode='redirect')
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status(),
'script': list(_SCP81_SCRIPT_BASE),
'script_kind': _SCP81_SCRIPT_KIND,
@@ -2121,7 +2139,7 @@ def _scp81_bip_control(body):
on_log=lambda kind, **fields: _BIP.log(kind, **fields))
_SCP81_MODE = 'dump'
_SCP81_TARGET = (_SCP81_LISTENER.host, _SCP81_LISTENER.port)
_BIP.enable(host, _SCP81_LISTENER.port)
_BIP.enable(host, _SCP81_LISTENER.port, mode='redirect')
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status()}