Skip to content

feat(minimald): networking clean up and DNS plumbing - #717

Merged
norrietaylor merged 8 commits into
mainfrom
feat/session-ingress-and-dns
Jul 13, 2026
Merged

feat(minimald): networking clean up and DNS plumbing#717
norrietaylor merged 8 commits into
mainfrom
feat/session-ingress-and-dns

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 10, 2026

Copy link
Copy Markdown
Member

The final network YOLO

Clean up networking state so that it is in defined state and ready for handover. The missing DNS plumbing for own-ip host resolutions is now in feature-complete state. Mesh and ssh-forwarding has been feature flagged out which removes their command line invocations.

Per-sandbox egress policy is still not completed and requires spec work.

Verification

Native, KVM and HVF deployments veriifed

  • A no-network session is fully isolated. Inside the sandbox there are no network interfaces at all; an outbound HTTP request fails and a DNS lookup fails.
  • A host-networked session can reach the internet. From inside the sandbox, an outbound HTTPS fetch returns 200 and a DNS name resolves.
  • An own-IP session can reach the internet through the per-host switch. An outbound fetch from inside the sandbox returns 200.
  • A published own-IP session is reachable by its hostname through the managed egress proxy. A request from the host for the session's *.min.internal name, routed via the egress proxy, returns 200.
  • Static ingress publishes a session's port on host loopback. A request from the host to the mapped external port reaches the server running inside the sandbox and returns 200.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an opt-in remote-access feature for mesh and SSH forwarding capabilities.
    • Added per-session inbound traffic controls for TCP and UDP connections.
    • Added automatic internal DNS registration for session network addresses.
    • Improved network control communication reliability when connections remain open.
  • Bug Fixes

    • Unauthorized inbound traffic is now rejected by default when it is not explicitly allowed.
    • SSH direct-tcpip forwarding fails closed when the forwarding feature is disabled.
  • Tests

    • Added native network-namespace integration coverage for key networking scenarios.

norrietaylor and others added 4 commits July 10, 2026 14:57
The forwarder control verbs (POST /services/forwarder/expose and the
:7654/:7655 proxy-publish) sent HTTP/1.0 with `Connection: close` and
read the reply to EOF. Over the KVM libkrun `add_vsock_port2(listen =
false)` shuttle, gvproxy's response is dropped when it closes the socket,
so the guest reads 0 bytes ("malformed gvproxy status line") even though
the forward was applied. apply_ingress then rolls back a *successful*
own-ip ingress attach, so the session's shell never starts (gap G-N8);
own-ip without ingress is unaffected.

Send HTTP/1.1 keep-alive (no `Connection: close`) and frame the reply by
Content-Length instead of read-to-EOF, so the server never closes first
and the response drains to the guest, which closes from its own side.
This also repairs the in-VM :7654/:7655 proxy-publish (same leg). The
native unix path (DM2) is unaffected.

Verified live on DM3 (KVM): own-ip + --ingress attach starts the shell
and publishes the lease; TC3/TC4/TC7/TC8 = 200/200/(401+200)/200, with
no startup proxy-publish warnings. Adds a regression test that hangs if
the framing reverts to read-to-EOF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… DNS

Two networking findings from a cross-platform (DM1/DM2) test run:

- #2: switch-side ingress was unenforced. `--ingress` only gated the
  host-loopback publish, so a peer session or the daemon tap could reach
  any port on an own-IP PTask over the shared gvproxy switch (gvproxy has
  no per-client ACL API). Add a stateless TCP-SYN gate to the inbound
  relay leg (`relay_switch_to_tap`): a bare SYN (SYN set, ACK clear) to a
  port not in the target's ingress policy is dropped; established/return
  traffic, egress, and declared ports pass. UDP and non-IPv4 pass through
  (documented limitation). Wires the previously-dead `PolicyWarnLimiter`
  as the R2.7 drop-log site.

- #3 / UC6: a session could not resolve a peer's `*.min.internal` name,
  and the own-IP lease rotates per attach. Register `<session>.<host-id>`
  to the current lease in gvproxy's runtime DNS (POST /services/dns/add)
  on every own-IP attach. gvproxy's resolver is the switch gateway every
  sandbox already queries, so no new resolver or resolv.conf change is
  needed; gvproxy's newest-wins merge picks up a rotated lease with no
  remove verb.

Peers can now reach declared ports on another session by hostname (UC6),
subject to that target's ingress (#2). Verified end-to-end on DM2 (the
name registers to the live lease) plus unit tests for the frame parser
and the DNS request shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate the SSH port-forward and WireGuard-mesh surfaces behind new,
off-by-default features so they compile out for now: UC7, UC2b option A,
and the ssh-forward fallback are deferred (the daemon does not yet consume
mesh enrolment, and ssh-forward's target resolves in the VM/host context
rather than the sandbox).

- New `remote-access` feature on `minimal` gates the `mesh` and
  `ssh-forward` (alias `forward`) subcommands and their plumbing.
- New `ssh-forward` feature on `minimald` gates the `direct-tcpip`
  handler; with it off, a `#[cfg(not)]` stub rejects every forward so the
  daemon fails closed rather than relying on russh's default handler.
- Drop `networking-wg` from the built features (justfile) and remove the
  WireGuard mesh test steps from CI and release. The mTLS reverse proxy
  (`networking-proxy`) is unaffected and stays enabled.

Both crates compile, clippy, and test clean with and without the new
features.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `test-plan.md` / `test-plan.sh` pair under `03-spec-networking/`
described an earlier DM1/DM2 CLI walk-through and is no longer maintained;
nothing in the build, CI, or code references it. The networking spec,
architecture, and diagram docs remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly matches the PR’s main themes: networking cleanup and DNS plumbing in minimald.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@norrietaylor
norrietaylor marked this pull request as draft July 10, 2026 22:38
Extends the switch-side ingress gate (finding #2) to UDP, which the
initial TCP-SYN gate left as a documented gap. UDP has no
connection-establishment signal, so a stateless port check cannot tell a
solicited reply (a DNS/QUIC response to an ephemeral port) from an
unsolicited datagram.

Add a per-PTask `UdpConntrack`: the egress relay leg records each outbound
datagram's reverse flow `(remote_ip, remote_port, local_port)`, and the
ingress leg allows a matching reply while dropping UDP to an undeclared
port that matches no flow. Declared UDP ingress ports (the internal port
of a UDP port-mapping) are always allowed. One flow table is shared
between the two relay tasks; entries expire after 120s with an
opportunistic sweep bounding the table.

Refactors the frame parse into `parse_ipv4_l4`, reused by the TCP and UDP
predicates. This closes UC6 for UDP round-trips (a peer reaching a
declared UDP port, and its reply, both work) and completes spec Req 6's
"TCP and UDP subject to target ingress."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@norrietaylor norrietaylor changed the title feat(minimald): session-to-session ingress enforcement + PTask DNS (#2/#3/UC6) feat(minimald): networking clean up and DNS plumbing Jul 10, 2026
…and-dns

# Conflicts:
#	.github/workflows/ci.yml
#	crates/minimal/src/lib.rs
@norrietaylor
norrietaylor force-pushed the feat/session-ingress-and-dns branch from 7449a87 to 21cd8f7 Compare July 10, 2026 23:31
Un-mothball the UC1 (no-net), UC4 (static ingress), and UC6 (PTask<->PTask
over the gvproxy switch) proofs in crates/minimald/tests/netns.rs, which lost
their CI coverage when the standalone ci-netns.yml was retired (#697). They
are Linux-native (DM2) integration tests that need only unprivileged userns +
sudo and a userspace gvproxy switch (no KVM), so they run as a
`netns-integration` job in the existing ci-linux-native lane rather than a
separate workflow / required check.

- Add the `netns-integration` job to ci-linux-native.yml (enable userns,
  fetch the pinned gvproxy, run `cargo test -p minimald --test netns
  -- --include-ignored`); wire it into the lane's success aggregator and add
  the gvproxy pin to the changes filter.
- Un-mothball netns.rs: update the header and #[ignore] reasons.

The WireGuard mesh proof (mesh_uc7, networking-wg) stays out — that feature is
disabled. The three proofs pass locally on this branch, confirming the new
switch-side ingress gate does not regress UC1/UC4/UC6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@norrietaylor
norrietaylor force-pushed the feat/session-ingress-and-dns branch from 21cd8f7 to 8064a0b Compare July 10, 2026 23:31
Drop three unreferenced networking items found by a workspace-wide
dead-code sweep. All are behaviour-neutral: an identical before/after
run of the native-daemon and KVM-VM networking test plans produced the
same results (in-sandbox isolation, host egress, own-IP egress, managed
DNS proxy, static ingress, and the mTLS reverse proxy all unchanged),
and the network-namespace enforcement proofs stayed green.

- guest.rs: a no-op `let _ = &cidr;` — `cidr` is already consumed by the
  "egress up" tracing call a few lines below, so the borrow-and-drop did
  nothing.
- session_host.rs / session.rs: the `SandboxLauncher::session` field (and
  its unjustified `#[allow(dead_code)]`) was constructed but never read
  and has no Drop side effect; remove it with the now-unused import, the
  struct initializer, and rename the launcher parameter to `_session`.
- net/mod.rs: the `GATEWAY_MAC` re-export had no consumer anywhere in the
  workspace (the switch crate uses its own constant internally).

clippy --workspace --all-targets -D warnings clean; minimald unit tests
pass; ssh-forward / networking-proxy / remote-access gated builds pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTLRizuWQ3GuZmBBv27Nt7
@norrietaylor
norrietaylor marked this pull request as ready for review July 11, 2026 19:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
crates/minimald/src/net/switch.rs (1)

626-653: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

UdpConntrack sweep can degrade to O(n) per packet under sustained high flow counts.

record_egress only sweeps when flows.len() > UDP_FLOW_SWEEP_AT; if live (unexpired) flows persist above that threshold, retain re-scans the entire map on every subsequent egress datagram rather than amortizing, since none of the still-fresh entries get removed.

Consider rate-limiting the sweep itself (e.g., only sweep once per N inserts or once per fixed interval) so a session with many concurrent UDP flows doesn't pay a full scan per packet indefinitely.

♻️ Sketch: amortize the sweep with a counter/interval instead of a length check alone
 struct UdpConntrack {
     flows: Mutex<HashMap<(Ipv4Addr, u16, u16), Instant>>,
+    last_sweep: Mutex<Instant>,
 }

Gate the retain call on now.duration_since(*last_sweep) > SOME_MIN_INTERVAL in addition to (or instead of) the length check, so it can't fire on every insert once the table is persistently large.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/net/switch.rs` around lines 626 - 653, Rate-limit the
cleanup in UdpConntrack::record_egress so a persistently large flow table does
not trigger flows.retain on every insert. Track the last sweep time or an
equivalent insert interval in UdpConntrack, and only sweep when the table
exceeds UDP_FLOW_SWEEP_AT and the configured minimum interval has elapsed;
preserve existing TTL-based removal and flow recording behavior.
crates/minimald/src/net/policy.rs (1)

340-355: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Body-size cap overflow is silently truncated, unlike the header-size cap.

Hitting MAX_CONTROL_RESPONSE while reading headers returns an explicit Err, but hitting it while reading the body just breaks the loop and returns whatever was buffered so far — with no signal that the response is incomplete. A non-2xx status in that case would report a truncated error body without indicating truncation, and a 2xx would be treated as fully successful even though the body was cut short.

🛡️ Suggested fix: surface the truncation as an error
     while remaining > 0 {
         if response.len() as u64 >= MAX_CONTROL_RESPONSE {
-            break;
+            return Err(io::Error::other(
+                "gvproxy control response exceeded the size cap before its body completed",
+            ));
         }
         let n = stream.read(&mut buf).await?;
         if n == 0 {
             break;
         }
         response.extend_from_slice(&buf[..n]);
         remaining = remaining.saturating_sub(n);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/net/policy.rs` around lines 340 - 355, Update the
body-reading loop in the response buffering flow to return an explicit error
when MAX_CONTROL_RESPONSE is reached before the Content-Length body is fully
buffered, instead of breaking and returning truncated data. Preserve normal EOF
handling and successful completion when the declared body length has been read.
crates/minimald/src/net/gvproxy_network.rs (2)

256-271: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Best-effort DNS registration still blocks the attach path synchronously.

Even though DNS registration failures are swallowed (tracing::warn! only), the .await on register_dns_name still runs inline before OwnIpGuard is returned, so a sluggish/unresponsive gvproxy control channel adds real latency (bounded by the 5s GVPROXY_CONTROL_TIMEOUT) to every own-IP session launch, purely for a "best-effort" side effect. Consider spawning this off as a detached background task so a hiccup here can't delay attach completion at all.

♻️ Suggested non-blocking registration
-    if let Err(e) = crate::net::policy::register_dns_name(
-        &control,
-        crate::net::dns::DEFAULT_HOST_ID,
-        session_name,
-        lease_ip,
-    )
-    .await
-    {
-        tracing::warn!(error = %e, session = session_name, "registering *.min.internal name on gvproxy");
-    }
+    let control_for_dns = control.clone(); // requires ControlChannel: Clone
+    let session_name_owned = session_name.to_string();
+    tokio::spawn(async move {
+        if let Err(e) = crate::net::policy::register_dns_name(
+            &control_for_dns,
+            crate::net::dns::DEFAULT_HOST_ID,
+            &session_name_owned,
+            lease_ip,
+        )
+        .await
+        {
+            tracing::warn!(error = %e, session = %session_name_owned, "registering *.min.internal name on gvproxy");
+        }
+    });

Please verify ControlChannel derives/implements Clone (or restructure to avoid needing it) before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/net/gvproxy_network.rs` around lines 256 - 271, Make the
DNS registration in the own-IP attach flow non-blocking by spawning it as a
detached background task instead of awaiting register_dns_name inline before
returning OwnIpGuard. Verify ControlChannel supports Clone and clone or
otherwise safely transfer the control handle into the task; retain the existing
best-effort error warning within the task.

256-271: 🗄️ Data Integrity & Integration | 🔵 Trivial

Stale DNS records never expire — no invalidation on teardown.

Register this PTask's <name>.<host-id>.min.internal → its current lease so peer sessions can resolve it (finding #3 / UC6). Done for every own-IP PTask, even with no ingress: resolvable names are how peers find each other, and the ingress gate independently governs reachability. Best-effort — a DNS hiccup must not fail an otherwise-working attach. OwnIpGuard::teardown only removes ingress forwards, never the DNS record (there's no remove verb per the dns_add_body doc comment), so a name keeps resolving to a now-freed lease until/unless something re-registers the same name. If that lease is recycled to a different session before the old name is ever re-registered, the stale record briefly points a peer at the wrong tenant (mitigated, but not eliminated, by the target's own ingress gate). Worth confirming this residual staleness window is an accepted tradeoff, and whether gvproxy's DNS table growth (records accumulate indefinitely across the daemon's lifetime with no reaping) is bounded in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/net/gvproxy_network.rs` around lines 256 - 271, Ensure
the DNS record registered by register_dns_name is invalidated when the
corresponding OwnIpGuard is torn down, rather than leaving stale lease mappings
behind. Add or use the appropriate gvproxy DNS removal/expiry mechanism
alongside OwnIpGuard::teardown, preserving best-effort error handling, and
verify records cannot accumulate indefinitely when sessions end.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/minimald/src/net/policy.rs`:
- Around line 176-233: Validate session_name before dns_add_body constructs the
DNS record label, ensuring it is a valid single DNS label and rejecting or
sanitizing dots and other invalid characters. Apply this through
register_dns_name or dns_add_body, while preserving the intended lowercased
"{session_name}.{host_id}" hostname for valid input.

---

Nitpick comments:
In `@crates/minimald/src/net/gvproxy_network.rs`:
- Around line 256-271: Make the DNS registration in the own-IP attach flow
non-blocking by spawning it as a detached background task instead of awaiting
register_dns_name inline before returning OwnIpGuard. Verify ControlChannel
supports Clone and clone or otherwise safely transfer the control handle into
the task; retain the existing best-effort error warning within the task.
- Around line 256-271: Ensure the DNS record registered by register_dns_name is
invalidated when the corresponding OwnIpGuard is torn down, rather than leaving
stale lease mappings behind. Add or use the appropriate gvproxy DNS
removal/expiry mechanism alongside OwnIpGuard::teardown, preserving best-effort
error handling, and verify records cannot accumulate indefinitely when sessions
end.

In `@crates/minimald/src/net/policy.rs`:
- Around line 340-355: Update the body-reading loop in the response buffering
flow to return an explicit error when MAX_CONTROL_RESPONSE is reached before the
Content-Length body is fully buffered, instead of breaking and returning
truncated data. Preserve normal EOF handling and successful completion when the
declared body length has been read.

In `@crates/minimald/src/net/switch.rs`:
- Around line 626-653: Rate-limit the cleanup in UdpConntrack::record_egress so
a persistently large flow table does not trigger flows.retain on every insert.
Track the last sweep time or an equivalent insert interval in UdpConntrack, and
only sweep when the table exceeds UDP_FLOW_SWEEP_AT and the configured minimum
interval has elapsed; preserve existing TTL-based removal and flow recording
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a11d1daf-1587-4f6a-b478-7daa66b2ed77

📥 Commits

Reviewing files that changed from the base of the PR and between 0281323 and 9f405ae.

📒 Files selected for processing (18)
  • .github/workflows/ci-linux-native.yml
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • crates/minimal/Cargo.toml
  • crates/minimal/src/lib.rs
  • crates/minimald/Cargo.toml
  • crates/minimald/src/connection.rs
  • crates/minimald/src/guest.rs
  • crates/minimald/src/net/gvproxy_network.rs
  • crates/minimald/src/net/mod.rs
  • crates/minimald/src/net/policy.rs
  • crates/minimald/src/net/switch.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/session_host.rs
  • crates/minimald/tests/netns.rs
  • docs/specs/03-spec-networking/test-plan.md
  • docs/specs/03-spec-networking/test-plan.sh
  • justfile
💤 Files with no reviewable changes (4)
  • docs/specs/03-spec-networking/test-plan.md
  • .github/workflows/release.yml
  • docs/specs/03-spec-networking/test-plan.sh
  • .github/workflows/ci.yml

Comment on lines +176 to +233
/// A gvproxy DNS zone-add request body for `POST /services/dns/add`
/// (gvproxy's `types.Zone`): registers `records` under the DNS zone `name`.
///
/// gvproxy merges on re-add with newest-first precedence and resolves to the
/// first matching record, so re-registering a name with a new IP makes the
/// newest win — how a rotated lease is picked up with no remove verb.
#[derive(Debug, Serialize)]
struct DnsZone {
name: String,
records: Vec<DnsRecord>,
}

/// A single `name → ip` answer within a [`DnsZone`].
#[derive(Debug, Serialize)]
struct DnsRecord {
name: String,
ip: String,
}

/// Registers `<session_name>.<host_id>` in gvproxy's `min.internal.` DNS zone,
/// pointing at the PTask's current switch lease (finding #3 / UC6).
///
/// gvproxy's resolver is the switch gateway (`100.64.0.1`) that every own-IP
/// sandbox's `resolv.conf` already targets, so this makes a PTask's
/// `*.min.internal` hostname resolvable *from a peer session* — with no new
/// resolver process and no `resolv.conf` change. The zone `Name` carries the
/// trailing dot gvproxy matches DNS queries against; the record label is
/// lowercased (gvproxy matches labels case-sensitively).
///
/// # Errors
///
/// Returns the I/O error from the gvproxy control request (non-2xx or transport).
pub async fn register_dns_name(
control: &ControlChannel,
host_id: &str,
session_name: &str,
lease_ip: Ipv4Addr,
) -> io::Result<()> {
post_json(
control,
"/services/dns/add",
&dns_add_body(host_id, session_name, lease_ip),
)
.await
}

/// Builds the `/services/dns/add` zone body for a PTask. Split out so the exact
/// wire shape (trailing-dot zone, lowercased label, dotted-quad IP) is unit-testable
/// without a live gvproxy.
fn dns_add_body(host_id: &str, session_name: &str, lease_ip: Ipv4Addr) -> DnsZone {
DnsZone {
name: format!("{}.", crate::net::dns::HOSTNAME_SUFFIX),
records: vec![DnsRecord {
name: format!("{session_name}.{host_id}").to_ascii_lowercase(),
ip: lease_ip.to_string(),
}],
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether session names are validated/constrained anywhere before reaching DNS registration.
rg -nP -C3 '\bfn\s+(validate_policy|validate_name|session_name)\b' crates/sessions crates/minimald

Repository: gominimal/minimal

Length of output: 857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and search for session name creation/validation and DNS registration call sites.
echo '--- crates/minimald/src/net/policy.rs outline ---'
ast-grep outline crates/minimald/src/net/policy.rs --view expanded || true

echo '--- session_name / register_dns_name occurrences ---'
rg -n -C 3 '\bsession_name\b|register_dns_name\(' crates/minimald crates/sessions

echo '--- potential session name type/constructor definitions ---'
rg -n -C 3 '\bstruct\s+Session\b|\bstruct\s+.*Session.*\b|\bnew_session\b|\bcreate_session\b|\bsession\s*name\b' crates/sessions crates/minimald

Repository: gominimal/minimal

Length of output: 43451


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- SessionConfig and any name validation ---'
rg -n -C 4 '\bSessionConfig\b|\bname\s*:\s*String\b|\bname\s*:\s*&str\b|\bvalidate_.*name\b|regex|^[[:space:]]*name[[:space:]]*[:=]' crates/minimald crates/sessions crates/minimald_rpc

echo '--- create_session boundary and config fields ---'
rg -n -C 4 '\bcreate_session\(' crates/minimald/src/sessions.rs crates/minimald/src/rpc.rs crates/minimald/src/session_host.rs crates/minimald/src/net/dns.rs crates/minimald/src/net/gvproxy_network.rs

echo '--- session name tests / invalid name cases ---'
rg -n -C 4 'invalid.*name|name.*invalid|session name|dot|hostname|label' crates/minimald crates/sessions

Repository: gominimal/minimal

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate minimald_rpc SessionConfig definition ---'
fd -a '.*' . | rg '/(minimald_rpc|rpc\.rs|lib\.rs|mod\.rs|types\.rs)$' | head -n 200

echo '--- SessionConfig occurrences with nearby context ---'
rg -n -C 3 'pub struct SessionConfig|SessionConfig \{' crates . --glob '!target/**' --glob '!dist/**' --glob '!build/**'

echo '--- name validation around create_session/build_record ---'
sed -n '60,110p' crates/minimald/src/sessions.rs
echo '---'
sed -n '960,1025p' crates/minimald/src/sessions.rs
echo '---'
sed -n '1,140p' crates/minimald/src/net/dns.rs

Repository: gominimal/minimal

Length of output: 24126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- CLI/session name definition ---'
rg -n -C 4 '\bname\b.*(String|Option<String>|Arg|value_parser|validator)|SessionConfig \{|args\.name|struct .*Args|clap' crates/minimal/src/lib.rs crates/minimald-rpc/src/lib.rs crates/minimald/src/sessions.rs

echo '--- minimald-rpc SessionConfig docs ---'
sed -n '188,220p' crates/minimald-rpc/src/lib.rs

echo '--- minimal CLI config wiring ---'
sed -n '790,825p' crates/minimal/src/lib.rs

Repository: gominimal/minimal

Length of output: 19514


Validate session_name before building the gvproxy DNS label. minimald_rpc::SessionConfig.name is free-form, and dns_add_body() concatenates it directly into "{session_name}.{host_id}" (crates/minimald/src/net/policy.rs:225-229). A name containing . or other invalid label characters will register a different hostname than intended. Reject or sanitize non-label input here, or enforce a stricter upstream guarantee.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/net/policy.rs` around lines 176 - 233, Validate
session_name before dns_add_body constructs the DNS record label, ensuring it is
a valid single DNS label and rejecting or sanitizing dots and other invalid
characters. Apply this through register_dns_name or dns_add_body, while
preserving the intended lowercased "{session_name}.{host_id}" hostname for valid
input.

@norrietaylor
norrietaylor merged commit 68d4352 into main Jul 13, 2026
28 checks passed
@norrietaylor
norrietaylor deleted the feat/session-ingress-and-dns branch July 13, 2026 16:50
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.

2 participants