fix(packet_capture): default waev.app brokers to a JWT TTL they accept (#248)

waev.app rejects a token whose exp is more than an hour past its iat, so the
project-wide 24-hour default could never authenticate there — the operator saw
only a bare auth failure with no hint that the lifetime was the problem, while
the same public key worked from meshcoretomqtt.

waev.app hosts now resolve to a 3600s TTL with renewal at 3500s when no value
was configured for them, per broker or globally; a configured value still wins,
and the substitution is logged. Matching is the apex and its subdomains only, so
a lookalike host like waev.app.example.com does not qualify.
This commit is contained in:
agessaman committed 2026-09-20 10:58:09 -07:00
1 parent c573755324
commit 4312d8a39c
2 files changed
+115 -1

No files matched your search

@@ -76,10 +76,23 @@ NEIGHBORS_ATTEMPT_STATE_KEY = "packet_capture.last_neighbors_attempt"
# instead, which is longer.
NEIGHBORS_RETRY_BACKOFF_SECONDS = 300.0
# waev.app rejects a JWT whose exp is more than an hour past its iat, so the
# project-wide 24-hour default cannot authenticate there (#248). These are the
# values its brokers accept, applied only when nothing else was configured;
# renewal is set just under the TTL so a token is replaced before it expires.
WAEV_JWT_TTL_SECONDS = 3600
WAEV_JWT_RENEWAL_INTERVAL = 3500
# Sentinel meaning "no IATA configured" (documented as invalid in config.ini.example).
DEFAULT_IATA = "XYZ"
def _is_waev_host(host: str) -> bool:
"""Whether ``host`` is a waev.app broker (the apex or any subdomain)."""
normalized = (host or "").strip().lower().rstrip(".")
return normalized == "waev.app" or normalized.endswith(".waev.app")
def _decode_key_str(key_str: str) -> Optional[bytes]:
"""Decode a 16-byte channel key from a hex (32 chars) or base64 string."""
key_str = key_str.strip()
@@ -689,6 +702,8 @@ class PacketCaptureService(BaseServicePlugin):
global_jwt_renewal = config.getint("PacketCapture", "jwt_renewal_interval", fallback=43200)
global_jwt_ttl = config.getint("PacketCapture", "jwt_ttl_seconds", fallback=86400)
global_renewal_set = config.has_option("PacketCapture", "jwt_renewal_interval")
global_ttl_set = config.has_option("PacketCapture", "jwt_ttl_seconds")
# Parse multiple brokers (mqtt1_*, mqtt2_*, etc.)
broker_num = 1
@@ -712,21 +727,42 @@ class PacketCaptureService(BaseServicePlugin):
if not upload_packet_types:
upload_packet_types = None
host = config.get("PacketCapture", server_key, fallback="localhost")
# waev.app refuses a token whose lifetime runs past an hour, so the
# 24-hour default never authenticates there and the operator only sees
# a bare auth failure (#248). Give those brokers a lifetime they accept
# unless a value was chosen for them, per broker or globally.
waev_default = _is_waev_host(host)
renew_key = f"mqtt{broker_num}_jwt_renewal_interval"
if config.has_option("PacketCapture", renew_key):
jwt_renewal_interval = config.getint("PacketCapture", renew_key)
elif waev_default and not global_renewal_set:
jwt_renewal_interval = WAEV_JWT_RENEWAL_INTERVAL
else:
jwt_renewal_interval = global_jwt_renewal
ttl_key = f"mqtt{broker_num}_jwt_ttl_seconds"
if config.has_option("PacketCapture", ttl_key):
jwt_ttl_seconds = config.getint("PacketCapture", ttl_key)
elif waev_default and not global_ttl_set:
jwt_ttl_seconds = WAEV_JWT_TTL_SECONDS
else:
jwt_ttl_seconds = global_jwt_ttl
if waev_default and (
jwt_ttl_seconds != global_jwt_ttl or jwt_renewal_interval != global_jwt_renewal
):
self.logger.info(
"MQTT broker %d (%s) is a waev.app host; using a %ds JWT TTL and %ds "
"renewal instead of the defaults, which it rejects. Set "
"mqtt%d_jwt_ttl_seconds to override.",
broker_num, host, jwt_ttl_seconds, jwt_renewal_interval, broker_num,
)
broker = {
"enabled": True,
"host": config.get("PacketCapture", server_key, fallback="localhost"),
"host": host,
"port": config.getint("PacketCapture", f"mqtt{broker_num}_port", fallback=1883),
"username": config.get("PacketCapture", f"mqtt{broker_num}_username", fallback=None),
"password": config.get("PacketCapture", f"mqtt{broker_num}_password", fallback=None),
@@ -124,3 +124,81 @@ def test_auth_token_iat_exp_uses_broker_then_global():
)
def test_jwt_ttl_log_phrase(ttl: int, phrase: str):
assert PacketCaptureService._jwt_ttl_log_phrase(ttl) == phrase
def _waev_service(ini: str):
bot = _bot_from_ini(ini)
svc = object.__new__(PacketCaptureService)
svc.bot = bot
svc.logger = MagicMock()
return svc, PacketCaptureService._parse_mqtt_brokers(svc, bot.config)
def test_waev_broker_defaults_to_a_ttl_the_broker_accepts():
"""waev.app rejects the 24-hour default, leaving the bot unable to auth (#248)."""
_, brokers = _waev_service(
"""
[PacketCapture]
enabled = false
mqtt1_server = mqtt-a.waev.app
mqtt2_server = broker.example
"""
)
assert brokers[0]["jwt_ttl_seconds"] == 3600
assert brokers[0]["jwt_renewal_interval"] == 3500
# Only waev hosts move; every other broker keeps the project default.
assert brokers[1]["jwt_ttl_seconds"] == 86400
assert brokers[1]["jwt_renewal_interval"] == 43200
@pytest.mark.parametrize("host", ["waev.app", "WAEV.APP", "mqtt-b.waev.app"])
def test_waev_default_matches_apex_and_subdomains(host: str):
_, brokers = _waev_service(
f"""
[PacketCapture]
enabled = false
mqtt1_server = {host}
"""
)
assert brokers[0]["jwt_ttl_seconds"] == 3600
@pytest.mark.parametrize("host", ["notwaev.app", "waev.app.example.com"])
def test_waev_default_does_not_match_lookalike_hosts(host: str):
_, brokers = _waev_service(
f"""
[PacketCapture]
enabled = false
mqtt1_server = {host}
"""
)
assert brokers[0]["jwt_ttl_seconds"] == 86400
def test_explicit_per_broker_ttl_beats_the_waev_default():
_, brokers = _waev_service(
"""
[PacketCapture]
enabled = false
mqtt1_server = mqtt-a.waev.app
mqtt1_jwt_ttl_seconds = 1800
mqtt1_jwt_renewal_interval = 900
"""
)
assert brokers[0]["jwt_ttl_seconds"] == 1800
assert brokers[0]["jwt_renewal_interval"] == 900
def test_explicit_global_ttl_beats_the_waev_default():
"""An operator who set a global value meant it, even for waev."""
_, brokers = _waev_service(
"""
[PacketCapture]
enabled = false
jwt_ttl_seconds = 7200
jwt_renewal_interval = 3600
mqtt1_server = mqtt-a.waev.app
"""
)
assert brokers[0]["jwt_ttl_seconds"] == 7200
assert brokers[0]["jwt_renewal_interval"] == 3600