Comparing main...upnp · n0-computer/patchbay · GitHub
Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: n0-computer/patchbay
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: n0-computer/patchbay
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: upnp
Choose a head ref
Checking mergeability… Don’t worry, you can still create the pull request.
  • 13 commits
  • 19 files changed
  • 2 contributors

Commits on Apr 23, 2026

  1. feat: foundation for port mapping server on routers

    Introduces the patchbay::portmap module with public PortmapMode and
    PortmapConfig types, plus the internal mapping registry that later
    protocol servers will share. The registry keys external slots by
    (proto, external_port), carries a secondary index for deduplicating
    repeated requests from the same internal socket across protocols,
    and supports targeted removal, expiry reaping, and bulk drain for
    shutdown. No protocol handlers yet; those land in follow-up commits.
    
    This commit keeps the public API surface off-ramped from existing
    presets. Every router remains portmap-off by default.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    ef04ba8 View commit details
    Browse the repository at this point in the history
  2. feat: dedicated ip portmap nftables table

    Port mapping DNAT rules land in their own table at priority -110,
    one step before dstnat. The separation means Router::set_nat_mode
    can flush the ip nat table as it does today without wiping active
    port mappings. Rules match on ip daddr <wan_ip> so hairpin traffic
    follows the same path as inbound WAN.
    
    Rendering is a pure function; unit tests cover empty, UDP, TCP, and
    multi-rule cases. The apply helper deletes the table before
    reinstalling so each call is an atomic swap.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    00a3c88 View commit details
    Browse the repository at this point in the history
  3. feat: NAT-PMP server on routers

    RouterBuilder::portmap(PortmapMode) starts an in-process server inside
    the router namespace that listens on UDP 5351 per RFC 6886. Devices on
    the downstream LAN can request UDP or TCP mappings through any RFC
    6886 client; portmapper's Client::probe() now reports nat_pmp=true and
    its update_local_port() yields an external (wan_ip, ext_port) that
    forwards inbound traffic to the requester.
    
    The server dispatches by version byte so PCP can land alongside in a
    later commit without a second socket. Off by default on every preset;
    callers opt in explicitly. Lifetime=0 deletes a mapping per section
    3.3 of the RFC. Clients outside the downstream CIDR receive
    NotAuthorizedOrRefused.
    
    An APDF NAT filter would otherwise drop DNAT'd inbound flows; the
    filter chain now accepts packets with ct status dnat so static port
    forwards survive.
    
    Includes integration tests that use portmapper as a client and
    validate end-to-end inbound UDP delivery.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    29aef4e View commit details
    Browse the repository at this point in the history
  4. feat: PCP server sharing UDP 5351 with NAT-PMP

    Extends the portmap dispatch loop to recognize PCP version 2 packets
    per RFC 6887. Scope is the Announce and Map opcodes, client-initiated,
    unicast only. Server-originated ANNOUNCE on UDP 5350 is out of scope
    because the portmapper client does not consume it.
    
    Map authentication uses the 12-byte client nonce: a repeat request for
    an existing (client, local_port, proto) tuple must present the same
    nonce or the server returns NotAuthorized. Lifetime 0 deletes per RFC
    6887 section 15, with the same nonce check. Protocol 0 (all protocols)
    is rejected with UnsuppProtocol; only UDP and TCP are mapped.
    
    Integration test uses portmapper's TCP path to validate an inbound TCP
    connection from the WAN reaches the device through a PCP-granted
    mapping.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    6a0139f View commit details
    Browse the repository at this point in the history
  5. feat: UPnP IGD server with SSDP and SOAP

    Adds the IGD:1 service. An SSDP responder joins the
    239.255.255.250:1900 multicast group on the router's downstream bridge
    and answers M-SEARCH discoveries. An HTTP server bound to an ephemeral
    TCP port on the downstream gateway serves the device description, the
    WANIPConnection SCPD, and handles the SOAP actions GetExternalIPAddress,
    AddPortMapping, AddAnyPortMapping, and DeletePortMapping.
    
    The HTTP implementation is hand-rolled because the surface is three
    fixed routes; pulling hyper and axum into the core patchbay crate is
    not justified for that surface area. SOAP bodies are parsed with a
    minimal tag extractor, sufficient for the well-formed payloads
    igd-next sends.
    
    AddPortMapping validates that NewInternalClient falls inside the
    router's downstream CIDR and returns error 606 otherwise. IGD:1
    lifetime 0 is treated as "server maximum" per the spec, distinct from
    NAT-PMP/PCP where 0 means delete; DeletePortMapping is the only way to
    revoke a UPnP mapping.
    
    Integration tests cover UPnP discovery plus inbound UDP delivery and a
    combined probe that validates all three protocols coexist on a single
    router when PortmapMode::All is selected.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    bb41c7d View commit details
    Browse the repository at this point in the history
  6. feat: Router::set_portmap dynamic op

    Adds a runtime reconfiguration method that matches the existing
    set_nat_mode and set_firewall pattern. Dropping to PortmapMode::None
    tears the server down and flushes the ip portmap table; switching
    between non-None modes replaces the server and forgets every active
    mapping so clients re-probe.
    
    Adds two regression tests. set_nat_mode_preserves_active_mapping
    validates the C1 finding from the plan review: because the portmap
    rules live in a dedicated ip portmap table rather than inside ip nat,
    a NAT mode change no longer discards active mappings. The second test
    covers the shutdown path of set_portmap.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    8eca87c View commit details
    Browse the repository at this point in the history
  7. refactor(portmap): consolidate shared context and trim module seams

    Moves the per-request context shared by NAT-PMP, PCP, and UPnP out of
    nat_pmp.rs and into server.rs as ServerContext. The old Context type
    was named after one protocol but consumed by three. Field visibility
    narrows from pub to pub(super) so mutation points stay explicit.
    
    Removes two const _: fn() sentinels that were silencing unused-import
    lints and the module-level #[allow(dead_code)] attributes that hid
    dead items in the foundation commit. Replaces the hand-rolled
    Opcode::from_byte matchers with strum::FromRepr.
    
    Adds an explicit threat-model note to portmap/mod.rs: source-IP
    authorization is adequate inside the simulator but not outside.
    PortmapConfig::from_mode now delegates to
    `impl From<PortmapMode> for PortmapConfig`, which is the idiomatic
    conversion direction.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    4bb7d28 View commit details
    Browse the repository at this point in the history
  8. fix(portmap): tighten UPnP and PCP authorization

    UPnP: AddPortMapping now requires NewInternalClient to equal the
    caller's source IP, not just be within the downstream CIDR. Without
    that, any LAN host could DNAT inbound traffic destined for the router
    to another LAN host's socket; real consumer firmware has been abused
    this way. DeletePortMapping gains a symmetric check so only the
    mapping's original owner can revoke it.
    
    UPnP HTTP: body size capped at 64 KiB before buffering, with HTTP 413
    for clients that declare a larger Content-Length. Header and body reads
    now honor 10-second timeouts, so a slowloris client that opens a
    connection and dribbles bytes no longer holds a task forever. The
    accept loop bounds concurrent connections at 128 via a semaphore;
    excess connections are dropped immediately rather than spawning an
    unbounded number of handler tasks. Short bodies (peer closes before
    Content-Length bytes arrive) now fail with an explicit error instead
    of passing a silently-truncated buffer into SOAP parsing.
    
    PCP: the client_addr field inside the PCP request header is now
    validated against the UDP packet's source IP. RFC 6887 section 8.1
    requires this check; a mismatch returns AddressMismatch (result code
    12) instead of the prior silent truncation.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    3f26969 View commit details
    Browse the repository at this point in the history
  9. fix(portmap): serialize nft applies and reap expired mappings

    nft applies previously ran without the registry lock held, so two
    concurrent allocate+apply sequences could land out of order and leave
    nftables reflecting an older snapshot than the registry. Every
    handler now calls ServerContext::apply_after_mutation while holding
    the registry guard, which both serializes applies and makes
    apply-failure rollback atomic: a just-created mapping whose nft apply
    fails is removed from the registry before the error propagates, so
    clients that receive an error code do not find an orphan entry
    later.
    
    Adds a background reaper that runs every 30 seconds, drops mappings
    whose deadline has elapsed, and regenerates the nft table. Without
    this, a client that walks away from a 2-hour lease kept the entry
    for the router's lifetime, which mattered only in long-running
    simulations but was a real correctness gap.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    2ac9bc6 View commit details
    Browse the repository at this point in the history
  10. test(portmap): cover UPnP and PCP delete paths

    Adds upnp_delete_drops_mapping and pcp_delete_drops_mapping. The
    UPnP test exercises the newly-added caller-ownership check because
    portmapper's deactivate() sends DeletePortMapping from the same
    device that created the mapping; the server accepts because peer ==
    internal_client. The PCP test validates lifetime=0 delete semantics
    per RFC 6887 section 15.
    
    Changes the NAT-PMP local-port-zero response from UnsupportedOpcode
    (5) to NotAuthorizedOrRefused (2). Zero is a valid opcode but not a
    valid local port; replying with an unrelated spec-compliance error
    misled clients into believing Map was unimplemented.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    1c11676 View commit details
    Browse the repository at this point in the history
  11. docs: record staff review findings and final summary in worklog

    Captures the four-reviewer round (Rust expert, distributed systems,
    safety/security, docs/QA), the opposing-stance review that decided
    which findings to implement versus defer, and the final verification
    state (workspace tests, clippy, fmt) for the portmapping branch.
    Frando committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    782e193 View commit details
    Browse the repository at this point in the history
  12. fix(portmap): use published portmapper dev-dep

    CI checks out patchbay alone, so the path dep on a sibling net-tools
    checkout fails manifest resolution. portmapper 0.16.0 is on
    crates.io; switch to the published version.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Frando and claude committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    5852383 View commit details
    Browse the repository at this point in the history
  13. fix(runner): collapse nested if into match guards

    Clippy 1.95 promotes collapsible_match from warning to error under
    CI's -D warnings. The two pre-existing matches in
    parse_step_failure_summary already carried nested `if !v.is_empty()`
    checks that clippy now rejects. Rewrite as match guards.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Frando and claude committed Apr 23, 2026
    Configuration menu
    Copy the full SHA
    7787c51 View commit details
    Browse the repository at this point in the history
Loading