Skip to content

Routed --forward-localhost reaches a host service that listens only on ::1 - #1032

Merged
ejc3 merged 2 commits into
mainfrom
forward-localhost-any-host-loopback
Oct 1, 2026
Merged

ejc3 merged 2 commits into
mainfrom
forward-localhost-any-host-loopback

Conversation

@ejc3

@ejc3 ejc3 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Stacked on: snapshot-run-publish (PR #1031).

In routed mode the host side of --forward-localhost dialled 127.0.0.1:<port> and nothing else, so a host service bound only to ::1 could not be reached from the guest. The MySQL that WWW tests use on a devserver listens on [::1]:3300 only.

What changes

  • First commit. The flag forwards the host's localhost, which is both loopback addresses. The host side dials 127.0.0.1, and ::1 when 127.0.0.1 refuses the connection. Only a refusal is followed by ::1: any other error is of a service that is there. With no service on either address, one message names the port and the outcome of both dials.
  • Second commit. A failed --forward-localhost start closes the listeners it had bound. It used to abort the relays already started without waiting for them, the pattern Routed networking binds a published port on the host address its mapping names #1030 removed from the port forwards.

Contract and impact

Production code in routed networking's localhost relay. A host service on 127.0.0.1 is reached as before. New: when 127.0.0.1 refuses, a listener on [::1]:<port> is reached, so the flag opens that port of both loopback addresses to the guest. The help text says so. Rootless networking is unchanged (#1026).

Minimum evidence

Unit tests with a red each, the network namespace tests as root with a red for each commit, the unit suite and lint at both commits, and test_forward_localhost_routed on a VM.

Not in this PR

Evidence

Routed --forward-localhost reaches a host service that listens only on ::1

Red, each with the behaviour it pins put back and the test kept:

  the function dialling 127.0.0.1 only
    tcp_proxy::tests::a_host_service_on_ipv6_loopback_only_is_reached
      the service on ::1 accepts: connecting to host loopback
  every 127.0.0.1 error followed by a dial of ::1
    tcp_proxy::tests::only_a_refusal_on_ipv4_loopback_is_followed_by_ipv6_loopback
      Connection timed out (os error 110)
  the ::1 outcome left to the error's chain
    tcp_proxy::tests::no_host_service_is_an_error_naming_both_loopback_addresses
      connecting to the host's loopback port 62485: 127.0.0.1:62485 gave Connection refused (os error 111), then [::1]:62485
  the relay dialling 127.0.0.1 only (as root, in a network namespace)
    tcp_proxy::tests::test_localhost_forward_relay_reaches_ipv6_loopback
      the relay should reach the host's ::1  left: []

Green on this commit:
  make test-unit
  Summary [  71.262s] 1375 tests run: 1375 passed (1 slow), 0 skipped
  make lint  rc 0

As root on the build host, at the top of this stack:
  make _test-root FILTER="-p fcvm --lib -E 'test(/^network::tcp_proxy::tests::test_/)'"
  Summary [   0.178s] 12 tests run: 12 passed, 736 skipped

As root on the build host, at the top of this stack with the commit that reads
--ipv6-prefix from FCVM_IPV6_PREFIX picked on top (the host's own addresses are
not a routable /64, so the routed tests take its delegated subnet from that
variable):
  make test-root FILTER="-E 'test(/test_port_forward_routed|test_clone_port_forward_routed|test_forward_localhost_routed/)'"
  Summary [  58.010s] 3 tests run: 3 passed, 1719 skipped
  PASS [   9.0s] fcvm::test_forward_localhost test_forward_localhost_routed
  PASS [  11.5s] fcvm::test_port_forward test_port_forward_routed
  PASS [  37.5s] fcvm::test_snapshot_clone test_clone_port_forward_routed
The unit tests hold the other loopback address at the same port with a bound
socket that does not listen, so the refusal there does not depend on what else
runs on the machine. A bind error other than "address in use" fails those
tests by name: they used to retry on any error, which never ends on a host
without ::1.

A failed --forward-localhost start closes the listeners it had bound

Red, with the function as it was (as root, in a network namespace):
  make _test-root FILTER="-p fcvm --lib -E 'test(/^network::tcp_proxy::tests::test_/)'"
  Summary [   5.086s] 11 tests run: 10 passed, 1 failed, 731 skipped
  tcp_proxy::tests::test_localhost_forward_bind_failure_leaves_no_listener
    the first port must be free again once the start has failed: operation in namespace /var/run/netns/test-lfb-846982
    Caused by: Address already in use (os error 98)

Green, the same command at the top of this stack:
  Summary [   0.178s] 12 tests run: 12 passed, 736 skipped

Green on this commit:
  make test-unit
  Summary [  71.281s] 1375 tests run: 1375 passed (1 slow), 0 skipped
  make lint  rc 0
  make _test-root FILTER=--no-run  rc 0 (compiles the root-only tests, runs none)

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 42 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: dc706b5e-447d-4aa3-a32d-c77ed7775341

📥 Commits

Reviewing files that changed from the base of the PR and between 2b90ee1 and db197db.

📒 Files selected for processing (7)
  • DESIGN.md
  • src/cli/args.rs
  • src/network/routed.rs
  • src/network/tcp_proxy.rs
  • src/state/types.rs
  • src/storage/snapshot.rs
  • tests/test_forward_localhost.rs
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T13:34:29.023484Z db197db Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ejc3 added 2 commits October 1, 2026 06:29
…n ::1

In routed mode the host side of --forward-localhost dialled 127.0.0.1:<port>
and nothing else. The guest's relay accepts on 127.0.0.1 and on ::1 and does
not carry over which of the two its client dialled, so a host service bound
only to ::1 could not be reached from the guest at all. The MySQL that WWW
tests use on a devserver (mysqld@3300) is one: it listens on [::1]:3300.

The flag forwards the host's localhost, which is both loopback addresses.
connect_host_loopback dials 127.0.0.1, and ::1 when 127.0.0.1 refuses the
connection. A refusal is the one error that says nothing listens there. A
timeout, or no local port left to dial from, is the error of a service that is
there, and that connection is not sent to whatever listens on ::1. With no
service on either address the error names the port and the outcome of both
dials in one message, because the relay logs an error's own text and not its
chain.

Rootless networking is unchanged: the guest's relay dials the gateway
10.0.2.2, which pasta maps to the host's 127.0.0.1 only (#1026).

Red, each with the behaviour it pins put back and the test kept:

  the function dialling 127.0.0.1 only
    tcp_proxy::tests::a_host_service_on_ipv6_loopback_only_is_reached
      the service on ::1 accepts: connecting to host loopback
  every 127.0.0.1 error followed by a dial of ::1
    tcp_proxy::tests::only_a_refusal_on_ipv4_loopback_is_followed_by_ipv6_loopback
      Connection timed out (os error 110)
  the ::1 outcome left to the error's chain
    tcp_proxy::tests::no_host_service_is_an_error_naming_both_loopback_addresses
      connecting to the host's loopback port 62485: 127.0.0.1:62485 gave Connection refused (os error 111), then [::1]:62485
  the relay dialling 127.0.0.1 only (as root, in a network namespace)
    tcp_proxy::tests::test_localhost_forward_relay_reaches_ipv6_loopback
      the relay should reach the host's ::1  left: []

Green on this commit:
  make test-unit
  Summary [  71.262s] 1375 tests run: 1375 passed (1 slow), 0 skipped
  make lint  rc 0

As root on the build host, at the top of this stack:
  make _test-root FILTER="-p fcvm --lib -E 'test(/^network::tcp_proxy::tests::test_/)'"
  Summary [   0.178s] 12 tests run: 12 passed, 736 skipped

As root on the build host, at the top of this stack with the commit that reads
--ipv6-prefix from FCVM_IPV6_PREFIX picked on top (the host's own addresses are
not a routable /64, so the routed tests take its delegated subnet from that
variable):
  make test-root FILTER="-E 'test(/test_port_forward_routed|test_clone_port_forward_routed|test_forward_localhost_routed/)'"
  Summary [  58.010s] 3 tests run: 3 passed, 1719 skipped
  PASS [   9.0s] fcvm::test_forward_localhost test_forward_localhost_routed
  PASS [  11.5s] fcvm::test_port_forward test_port_forward_routed
  PASS [  37.5s] fcvm::test_snapshot_clone test_clone_port_forward_routed
The unit tests hold the other loopback address at the same port with a bound
socket that does not listen, so the refusal there does not depend on what else
runs on the machine. A bind error other than "address in use" fails those
tests by name: they used to retry on any error, which never ends on a host
without ::1.
start_localhost_forwards started a relay for each port as it went. When a
later port could not be bound it aborted the relays already started and
returned the error. abort only asks the runtime to drop a task, so their
listeners were still open when the error was returned. start_port_forwards had
the same shape until it bound every listener before starting any relay.

start_localhost_forwards now does the same. It binds every port first, and the
listeners are plain values until a relay takes them, so an error drops, and
with that closes, the ones already bound.

Red, with the function as it was (as root, in a network namespace):
  make _test-root FILTER="-p fcvm --lib -E 'test(/^network::tcp_proxy::tests::test_/)'"
  Summary [   5.086s] 11 tests run: 10 passed, 1 failed, 731 skipped
  tcp_proxy::tests::test_localhost_forward_bind_failure_leaves_no_listener
    the first port must be free again once the start has failed: operation in namespace /var/run/netns/test-lfb-846982
    Caused by: Address already in use (os error 98)

Green, the same command at the top of this stack:
  Summary [   0.178s] 12 tests run: 12 passed, 736 skipped

Green on this commit:
  make test-unit
  Summary [  71.281s] 1375 tests run: 1375 passed (1 slow), 0 skipped
  make lint  rc 0
  make _test-root FILTER=--no-run  rc 0 (compiles the root-only tests, runs none)
@ejc3
ejc3 force-pushed the snapshot-run-publish branch from 93388d5 to 9ad4b7e Compare October 1, 2026 13:31
@ejc3
ejc3 force-pushed the forward-localhost-any-host-loopback branch from dee5355 to db197db Compare October 1, 2026 13:31
@ejc3

ejc3 commented Oct 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: db197dbc7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Base automatically changed from snapshot-run-publish to main October 1, 2026 15:08

@ejc3 ejc3 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

NOT-A-DEFECT: CodeRabbit's comment on this PR is its rate-limit notice ("Review limit reached"), so it reviewed nothing here and claims nothing. Codex reviewed the head, db197db, and found no major issue.

@ejc3
ejc3 merged commit 9813cd7 into main Oct 1, 2026
14 checks passed
@ejc3
ejc3 deleted the forward-localhost-any-host-loopback branch October 1, 2026 15:28
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