Files
Rightup ae68112377 - Refactored ACL authentication flow with shared helpers for replay detection and session updates.
- Changed blank-password login behavior to create/manage guest sessions (when read-only is enabled), including replay protection and session timestamp tracking.
- Preserved and centralized `sync_since` handling during successful auth/session updates.
- Updated login handling to return explicit `(success, permissions)` and reset room-server sync guard state in SQLite on successful room-server login.
- Updated identity/banner output in mesh CLI from `pyMC_*` to `openHop_*` and adjusted default version fallback.
- Extended path handling to support encoded path-length semantics via `PathUtils`, with legacy fallback support for older/malformed packets.
- Added bundled ACK extraction from PATH payload extras and callback-based ACK propagation.
- Hardened room-server push logic: max-failure early skip, corrected expected ACK CRC calculation, path encoding compatibility, persisted sync/last-activity fields, and passed expected CRC + timeout into packet injection.
- Enhanced packet router ACK flow with dispatcher ACK registration helper, support for multipart ACK wrapper handling, configurable ACK wait timeout/CRC in injection, and ensured PATH helper processing always runs.
- Updated daemon wiring to pass ACK callback into `PathHelper`,
- make dispatcher dedupe configurable (prevent dbl deduping), and only register duplicate logging hook when dedupe is enabled.
- Expanded tests to cover guest blank-password replay tracking, updated room-server injector ACK expectations, and new duplicate-logging-hook gating behavior.
2026-07-02 16:32:32 +01:00

135 lines
5.2 KiB
Python

import asyncio
import logging
import time
logger = logging.getLogger("PathHelper")
class PathHelper:
def __init__(self, acl_dict=None, log_fn=None, ack_received_callback=None):
self.acl_dict = acl_dict or {}
self.log_fn = log_fn or logger.info
self.ack_received_callback = ack_received_callback
async def _register_ack_crc(self, ack_crc: int) -> None:
"""Propagate an ACK CRC to the configured callback."""
if ack_crc is None:
return
callback = self.ack_received_callback
if callback is None:
return
try:
result = callback(ack_crc)
if asyncio.iscoroutine(result):
await result
except Exception as e:
logger.debug(f"ACK callback failed for CRC {ack_crc:08X}: {e}")
async def process_path_packet(self, packet):
from openhop_core.protocol.constants import PAYLOAD_TYPE_ACK
from openhop_core.protocol.crypto import CryptoUtils
from openhop_core.protocol.packet_utils import PathUtils
try:
if len(packet.payload) < 2:
return False
dest_hash = packet.payload[0]
src_hash = packet.payload[1]
# Get the ACL for this destination identity
identity_acl = self.acl_dict.get(dest_hash)
if not identity_acl:
logger.debug(f"No ACL for dest 0x{dest_hash:02X}, allowing forward")
return False
# Find the client by source hash
client = None
for client_info in identity_acl.get_all_clients():
pubkey = client_info.id.get_public_key()
if pubkey[0] == src_hash:
client = client_info
break
if not client:
logger.debug(f"PATH packet from unknown client 0x{src_hash:02X}, allowing forward")
return False
# Get shared secret for decryption
shared_secret = client.shared_secret
if not shared_secret or len(shared_secret) == 0:
logger.debug(f"No shared secret for client 0x{src_hash:02X}, cannot decrypt PATH")
return False
# Decrypt the PATH packet payload
# Payload format: dest_hash(1) + src_hash(1) + mac(2) + encrypted_data
if len(packet.payload) < 4:
logger.debug(f"PATH packet too short: {len(packet.payload)} bytes")
return False
mac_and_data = packet.payload[2:] # Skip dest_hash and src_hash
aes_key = shared_secret[:16]
decrypted = CryptoUtils.mac_then_decrypt(aes_key, shared_secret, mac_and_data)
if not decrypted:
logger.debug(f"Failed to decrypt PATH packet from 0x{src_hash:02X}")
return False
# Parse decrypted PATH data
# Format: path_len(1) + path[path_byte_len] + extra_type(1) + extra[...]
if len(decrypted) < 1:
logger.debug("Decrypted PATH data too short")
return False
path_len_byte = decrypted[0]
if PathUtils.is_valid_path_len(path_len_byte):
path_byte_len = PathUtils.get_path_byte_len(path_len_byte)
path_hops = PathUtils.get_path_hash_count(path_len_byte)
else:
# Legacy fallback for malformed/old packets: treat first byte as raw path bytes.
path_byte_len = path_len_byte
path_hops = path_byte_len
if len(decrypted) < 1 + path_byte_len:
logger.debug(
f"PATH data truncated: need {1 + path_byte_len} bytes, got {len(decrypted)}"
)
return False
path_data = decrypted[1 : 1 + path_byte_len]
# Update client's out_path (same as C++ memcpy)
client.out_path = bytearray(path_data)
client.out_path_len = (
path_len_byte if PathUtils.is_valid_path_len(path_len_byte) else path_byte_len
)
client.last_activity = int(time.time())
logger.info(
f"Updated out_path for client 0x{src_hash:02X} -> 0x{dest_hash:02X}: "
f"path_len_byte=0x{path_len_byte:02X}, hops={path_hops}, "
f"path={[hex(b) for b in path_data]}"
)
# Handle bundled ACK in PATH extra section.
ack_crc = None
extra_start = 1 + path_byte_len
if len(decrypted) > extra_start:
extra_type = decrypted[extra_start] & 0x0F
extra_payload = decrypted[extra_start + 1 :]
if extra_type == PAYLOAD_TYPE_ACK and len(extra_payload) >= 4:
ack_crc = int.from_bytes(extra_payload[:4], "little")
logger.info(
f"PATH bundled ACK extracted for client 0x{src_hash:02X}: CRC={ack_crc:08X}"
)
if ack_crc is not None:
await self._register_ack_crc(ack_crc)
# Don't mark as do_not_retransmit - let it forward normally
return False
except Exception as e:
logger.error(f"Error processing PATH packet: {e}", exc_info=True)
return False