Skip to content

feat(iroh): publish signed direct UDP ports - #8480

Merged
azooz2003-bit merged 6 commits into
feat-iroh-final-integrationfrom
feat-iroh-direct-ports-web
Jul 19, 2026
Merged

azooz2003-bit merged 6 commits into
feat-iroh-final-integrationfrom
feat-iroh-direct-ports-web

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 19, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on feat-iroh-final-integration.

Adds the broker half of authenticated direct-path support:

  • accepts optional signed directPorts with independent IPv4 and IPv6 UDP ports
  • validates integer ports in 1...65535 and rejects empty or expanded objects
  • persists nullable family-specific ports with database constraints
  • publishes direct_ports only to authenticated same-account discovery
  • clears stale metadata when a legacy registration refreshes without the field
  • stores no private IP addresses

Verification:

  • bun test tests/iroh-*.test.ts tests/relay-token-route.test.ts: 110 pass, 29 DB-gated skips, 0 fail
  • real Postgres tests/iroh-db-behavior.test.ts: 29 pass, 0 fail
  • bun run typecheck: pass
  • focused ESLint: pass
  • bun run db:check: pass
  • migration applied twice successfully

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds signed direct-path UDP ports to the Iroh broker. Stores per-family ports, publishes them only to authenticated same-account discovery, and scrubs them on revocation and retention.

  • New Features

    • Accept directPorts with ipv4/ipv6 integers (1–65535) in signed registrations; reject empty or invalid shapes.
    • Persist as direct_port_v4/direct_port_v6; clear both when a refresh omits directPorts.
    • Publish direct_ports only to same‑account discovery; never store private addresses.
    • Clear direct_port_v4/direct_port_v6 on binding revocation and during retention drain of revoked bindings.
  • Migration

    • Apply 20260719120000_iroh_direct_ports to add direct_port_v4/direct_port_v6 with range checks.
    • Additive and optional; legacy clients continue to work.

Written for commit 89b9dd9. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 25dcd8f0-4c6a-4ab1-9b0a-1827ef889714

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-iroh-direct-ports-web

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds broker-side support for signed direct UDP port advertisements in the Iroh trust broker. When clients include a directPorts object (IPv4/IPv6 port integers) in their signed registration payload, the broker validates, persists, and returns those ports only to authenticated same-account discovery.

  • Model: adds IrohDirectPorts type, parseIrohDirectPorts validator (rejects empty, array, non-object, and out-of-range), and a udpPort helper enforcing 1–65535; both insertion paths in repository.ts now map directPorts → directPortV4/directPortV6 (nulling both on omission for clean legacy refresh).
  • Schema / migration: additive nullable columns with matching DB-level CHECK constraints; legacy clients continue to work with no change.
  • Publication policy: publicBinding conditionally emits direct_ports only when at least one family is non-null; both register (own binding) and discover (same-account) use this path — cross-account discover is scoped by userId in discoverySnapshot and never receives ports.
  • Retention: revokeActiveBindings immediately nulls ports; the revokedHints retention sweep is extended to pick up any revoked bindings with non-null ports as a defensive backstop.

Confidence Score: 5/5

Safe to merge — the change is purely additive, cryptographically scoped, and correctly isolated to same-account discovery.

All three layers of the feature (validation, persistence, publication) are consistent and well-guarded. Ports are validated at the application layer (1–65535, integer, non-empty) and enforced again by DB CHECK constraints. Publication is strictly same-account: publicBinding is only called from register (own binding) and discover (userId-scoped snapshot), so cross-account paths never see the field. Revocation immediately nulls the ports, and the retention sweep now covers them as a backstop. The migration is additive with no breaking changes for legacy clients.

No files require special attention.

Important Files Changed

Filename Overview
web/db/migrations/20260719120000_iroh_direct_ports/migration.sql Additive migration: adds nullable direct_port_v4/v6 integer columns with 1–65535 CHECK constraints; fully idempotent and non-breaking for legacy clients.
web/db/schema.ts Schema columns and check constraints match migration exactly; placed correctly relative to pathHints columns.
web/services/iroh/model.ts Adds IrohDirectPorts type, parseIrohDirectPorts validator, and udpPort helper; rejects null, array, empty object, and out-of-range values; unknown-key rejection and allowlist updated correctly.
web/services/iroh/repository.ts Both create and refresh paths write directPortV4/V6 (nulling on omission); revokeActiveBindings immediately clears ports; retention sweep extended to pick up revoked bindings with non-null ports as a backstop.
web/services/iroh/trustBroker.ts publicBinding conditionally emits direct_ports; called only from register (own binding) and discover (same-account snapshot) — cross-account paths never see ports.
web/tests/iroh-db-behavior.test.ts Comprehensive DB-gated tests: persists/updates/clears ports across registrations, enforces constraint boundaries, verifies revocation scrubs, and confirms retention sweep picks up legacy revoked bindings with stale ports.
web/tests/iroh-model-crypto.test.ts Unit tests cover valid port combinations, legacy omission, and a comprehensive rejection matrix (boundary values, fractions, extra keys, empty object).
web/tests/iroh-trust-broker.test.ts In-memory broker tests verify publication scoping (same-account vs cross-account), update and clear semantics, and correct MemoryRepository modeling of the new fields.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant TrustBroker
    participant Model
    participant Repository
    participant DB

    Client->>TrustBroker: register(userId, signedPayload)
    TrustBroker->>Model: parseRegistrationPayload(raw)
    Model-->>TrustBroker: IrohRegistrationPayload
    TrustBroker->>Repository: consumeChallengeAndRegister(payload)
    Repository->>DB: INSERT/UPDATE iroh_endpoint_bindings
    DB-->>Repository: IrohBindingRecord
    Repository-->>TrustBroker: binding
    TrustBroker-->>Client: binding with direct_ports

    Client->>TrustBroker: discover(userId)
    TrustBroker->>Repository: discoverySnapshot userId-scoped
    Repository->>DB: SELECT same-account bindings
    DB-->>Repository: bindings
    TrustBroker-->>Client: bindings with direct_ports

    Client->>TrustBroker: revoke(userId, bindingId)
    TrustBroker->>Repository: revokeBinding
    Repository->>DB: clear ports and path_hints
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant TrustBroker
    participant Model
    participant Repository
    participant DB

    Client->>TrustBroker: register(userId, signedPayload)
    TrustBroker->>Model: parseRegistrationPayload(raw)
    Model-->>TrustBroker: IrohRegistrationPayload
    TrustBroker->>Repository: consumeChallengeAndRegister(payload)
    Repository->>DB: INSERT/UPDATE iroh_endpoint_bindings
    DB-->>Repository: IrohBindingRecord
    Repository-->>TrustBroker: binding
    TrustBroker-->>Client: binding with direct_ports

    Client->>TrustBroker: discover(userId)
    TrustBroker->>Repository: discoverySnapshot userId-scoped
    Repository->>DB: SELECT same-account bindings
    DB-->>Repository: bindings
    TrustBroker-->>Client: bindings with direct_ports

    Client->>TrustBroker: revoke(userId, bindingId)
    TrustBroker->>Repository: revokeBinding
    Repository->>DB: clear ports and path_hints
Loading

Reviews (2): Last reviewed commit: "fix(iroh): scrub revoked direct ports" | Re-trigger Greptile

Comment on lines +577 to +582
function udpPort(value: unknown): number {
if (!Number.isInteger(value) || (value as number) < 1 || (value as number) > 65_535) {
throw new IrohInvalidInputError({ code: "invalid_direct_ports" });
}
return value as number;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Every other numeric validator in this file (positiveInteger) uses Number.isSafeInteger. While the upper bound > 65_535 makes Number.isInteger functionally equivalent here (no unsafe integer can be ≤ 65535), the inconsistency could mislead future maintainers into using Number.isInteger for a larger bounded range where unsafe integers could slip through.

Suggested change
function udpPort(value: unknown): number {
if (!Number.isInteger(value) || (value as number) < 1 || (value as number) > 65_535) {
throw new IrohInvalidInputError({ code: "invalid_direct_ports" });
}
return value as number;
}
function udpPort(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 65_535) {
throw new IrohInvalidInputError({ code: "invalid_direct_ports" });
}
return value as number;
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@azooz2003-bit
azooz2003-bit merged commit 54a5dba into feat-iroh-final-integration Jul 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant