Skip to content

Adopt rift#1517: spawn-mode imposters inherit --host 127.0.0.1 #321

Closed

Description

@EtaCassiopeia

RiftProcess.buildCommand always passes --host (SpawnOptions.host, default 127.0.0.1). From the rift release carrying achird-labs/rift#1517, host-less imposters bind that host, as Mountebank's do, so spawn-mode imposters become loopback-only. A system under test running in a container (host.docker.internal, host-gateway) can no longer reach them.

  • Document this against that engine release.
  • Grep the ITs for container-to-spawned-imposter traffic.
  • Add a SpawnOptions note pointing at per-imposter host("0.0.0.0").

rift-java-testcontainers passes no --host and is unaffected.

Provenance: rift#1517 (PR achird-labs/rift#1541).


Triage

Verdict: build, docs and one IT only. Gate it on the first rift release after 0.22.0, and land it in the engine-bump PR. No SDK behaviour change. Route: Sonnet.

Engine facts (checked against v0.22.0 and origin/master @ 08f7bef2)

  • rift#1517 is fixed by PR rift#1541, merge commit 08f7bef2, merged 2026-10-16. git tag --contains 08f7bef2 is empty, so no release carries it yet.
  • v0.22.0 (the current pin): a host-less imposter binds 0.0.0.0 whatever --host says (crates/rift-mock-core/src/imposter/manager.rs:884, and :1188-1192 on rebind). --host only moves the admin API.
  • origin/master: ImposterManager::with_default_host (manager.rs:489) and bind_host_for (:495, used at :939 and :1260) make a host-less imposter bind the server's --host. An imposter's own host still wins, and the inherited host is never written into the config (so GET, ?replayable and the datadir are unchanged). It is wired only in the CLI server: crates/rift-http-proxy/src/server.rs:997 .with_default_host(cli.host.clone()).
  • --local-only pins the admin listener to loopback (server.rs:1421) but does not narrow imposter binds. Under the SDK default (--host 127.0.0.1 --local-only), the next release therefore puts host-less imposters on 127.0.0.1 only.
  • Embedded (FFI) builds ImposterManager::new() with no default host (crates/rift-ffi/src/lib.rs:307, :2408; with_default_host is never called there). Embedded mode is unaffected, and adminHost does not leak into imposters.
  • The Docker image gets no --host, so RiftContainer is unaffected (rift-java-testcontainers/.../RiftContainer.java sets no MB_HOST or --host).
  • The only wire-visible change is the host description text in sdk-conformance/schema/imposter.schema.json. The schema IT (adopt: validate DSL output against the engine's imposter JSON Schema (sdk-conformance schema/imposter.schema.json) #314) validates shape, not descriptions.

rift-java today

  • RiftProcess.buildCommand always passes --host opts.host() (rift-java-core/.../spawn/RiftProcess.java:211-212). The default is 127.0.0.1 (SpawnOptions.java:162), and localOnly defaults to true.
  • SpawnOptions.Builder.host javadoc (SpawnOptions.java:200-206) says it is "the interface the engine's admin API binds" and that localOnly(true) "makes the engine bind loopback whatever this says". After #1517 the second claim is wrong for imposters: host sets the bind of every host-less imposter even with localOnly on. The same wording appears in docs/design/sdk-api.md:185-186.
  • The SDK's own uri() stays correct. A host-less imposter resolves through ConnectOptions.defaultHostResolver to the admin host (ConnectOptions.java:155-158), and that host is what the imposter now binds. An explicit host is honoured by ImposterImpl (ImposterImpl.java:59, :78-80). No SDK code path breaks.
  • Grep for container-to-spawned-imposter traffic (host.docker.internal, host-gateway, exposeHostPorts, withExtraHost, NetworkInterface, getLocalHost() across rift-java-*/src): no IT or conformance test reaches a spawned imposter from a container or a non-loopback address. RiftContainerForwardTargetIT goes container to container. The host.docker.internal hits (docs/intercept.md:357, InterceptOptionsAttachTest.java:56) are about the intercept listener, which has its own explicit InterceptOptions.host("0.0.0.0") and is not an imposter.
  • Stale comments, all correct for 0.22.0 and needing an update with the bump: Ipv6HostIT.java:70 ("The engine binds an imposter on 0.0.0.0 ... whatever the admin host is"), and the ImposterUriHostTest.java:18-21 class doc ("default 0.0.0.0 ... whatever interface the admin API uses").
  • rift-scala mirrors SpawnOptions in bridge/src/main/scala/rift/bridge/Config.scala:150-153 (host = "127.0.0.1", localOnly = true). It inherits the behaviour, and its scaladoc for host needs the same note. No code change is needed there.

Decision

  • The issue's premise is correct: spawn-mode host-less imposters become loopback-only on the next engine release. Embedded and Testcontainers are unaffected. Two additions:
    1. "Grep the ITs" turns up nothing that breaks. The only change in this repo is documentation plus stale comments.
    2. The SpawnOptions javadoc is not only incomplete but becomes wrong. Its "localOnly makes it loopback whatever this says" does not hold for imposters.
  • The change does belong in the SDK, as documentation only. The engine's new default is the Mountebank-compatible, safer one, and the SDK's own default (127.0.0.1 + localOnly) signals "keep it local". Do not have the SDK drop --host or override it to keep the old all-interfaces imposter bind. That would silently undo an upstream security fix.
  • Do not add a spawn-wide "imposter host" option: the engine has no separate flag for one. Do not recommend SpawnOptions.host("0.0.0.0") as the container recipe either. It does widen host-less imposters, but RiftProcess.java:61 then builds the admin URI as http://0.0.0.0:port, which fails to connect on Windows. Per-imposter host("0.0.0.0") is the recipe.

Design

Replace the SpawnOptions.Builder.host javadoc (SpawnOptions.java:200-206) with:

/**
 * The engine's {@code --host}, default {@code 127.0.0.1}: an IP literal, including IPv6 {@code ::1}
 * bare or bracketed (a bare one needs rift ≥ 0.18.0). The admin URI is built from it, bracketed
 * when it is IPv6. {@link #localOnly(boolean)} (on by default) pins the admin API to loopback
 * whatever this says.
 *
 * <p>From rift &lt;NEXT&gt; (rift#1517), an imposter created without its own {@code host} also binds
 * this address, as in Mountebank, so by default spawned imposters are loopback-only and
 * {@code localOnly} does not change that. A system under test in a container
 * ({@code host.docker.internal}, host-gateway) must reach an imposter that names its own interface:
 * {@code imposter("x").host("0.0.0.0")}. Before that release, host-less imposters bind every
 * interface whatever this says.
 */

<NEXT> is the actual version of the first release after 0.22.0. Mirror the text in docs/design/sdk-api.md:185-186, and add a short "Reaching a spawned imposter from a container" note to docs/testcontainers.md, next to the existing .host("0.0.0.0") intercept example at :121.

Implementation plan

  • pom.xml: <rift.engine.version> bump to the release carrying 08f7bef2 (done by the bump PR; this issue rides on it)
  • rift-java-core/src/main/java/io/github/achirdlabs/rift/SpawnOptions.java: javadoc above
  • docs/design/sdk-api.md: SpawnOptions host comment
  • docs/testcontainers.md (or docs/index.md spawn section): container-reachability note with a per-imposter host("0.0.0.0") snippet
  • rift-java-conformance/src/test/java/io/github/achirdlabs/rift/conformance/Ipv6HostIT.java:70: fix the comment (host-less imposters now follow the spawn --host)
  • rift-java-core/src/test/java/io/github/achirdlabs/rift/ImposterUriHostTest.java:18-21: fix the class doc
  • new rift-java-conformance/src/test/java/io/github/achirdlabs/rift/conformance/SpawnHostInheritanceIT.java (below)
  • CHANGELOG/release notes: "spawn-mode host-less imposters are now loopback-only (engine rift#1517)"
  • follow-up on rift-scala: same note on SpawnConfig.host scaladoc (bridge/.../Config.scala:150)

Tests

  • Unit tests: none needed. RiftProcessTest already pins --host in argv (RiftProcessTest.java:25, :38), and argv does not change.
  • Live IT SpawnHostInheritanceIT, SPAWN lane (gatedTo(ConformanceTransport.SPAWN, ...), needs RIFT_IT=1), skipped when the engine version is below the release (EngineVersion.atLeast(version, "<NEXT>")):
    • On a default Rift.spawn(), a host-less imposter answers at imposter.uri() (127.0.0.1) and refuses on [::1] when IPv6 loopback exists. This is the same probe as the engine's manager.rs default_host tests.
    • The same imposter with .host("0.0.0.0") answers on a non-loopback local IPv4 address when one exists (pick the first site-local address from NetworkInterface, or assumeTrue if there is none), and the host-less one refuses there.
  • Embedded: no new test. Optionally add an assertion to an existing embedded IT (surefire) that a host-less imposter on adminHost("127.0.0.1") is still reachable on a non-loopback address, which pins "FFI does not inherit".

Acceptance criteria

  • SpawnOptions javadoc and sdk-api.md say that host sets the bind of host-less imposters from the named release, that localOnly does not narrow it, and that per-imposter host("0.0.0.0") is the container recipe.
  • Stale "binds 0.0.0.0 whatever the admin host" comments are gone.
  • SpawnHostInheritanceIT passes against the bumped engine with RIFT_IT=1, and is skipped on 0.22.0.
  • No change to RiftProcess.buildCommand, SpawnOptions defaults, embedded or Testcontainers behaviour.

Gating

Requires the first rift release after 0.22.0 (the one containing 08f7bef2). The javadoc has to name that version, and the IT can only pass on it, so everything ships in or right after the engine-bump PR. Nothing needs to land now: on the 0.22.0 pin the current docs are still accurate.

Activity

  1. added
    ready-to-implementTriaged: root cause confirmed, design and implementation steps recorded
    blockedBlocked on an external dependency (engine release, upstream issue)
    on Oct 17, 2026
  2. EtaCassiopeia commented on Oct 17, 2026

    @EtaCassiopeia
    CollaboratorAuthor

    Amendment from the 0.23.0 impact survey: rift#1523 (PR achird-labs/rift#1543) makes --local-only refuse non-loopback clients on imposters too, not only on the admin API. The SDK's spawn default is localOnly=true, so on Linux a container that reaches the host through the bridge (172.17.0.1) is refused even with host("0.0.0.0") on the imposter. Docker Desktop is still admitted, because its traffic arrives on loopback.

    The container recipe this issue documents must therefore say both:

    • use host("0.0.0.0") on the imposter;
    • on Linux, use SpawnOptions.builder().localOnly(false).

    The SpawnOptions.host javadoc, the Ipv6HostIT.java:70 comment, the ImposterUriHostTest class doc and sdk-api.md also describe the pre-0.23 binding and need updating. rift 0.23.0 is now released (it contains #1541 and #1543), so the blocked label comes off once the pin bump merges.

  3. removed
    blockedBlocked on an external dependency (engine release, upstream issue)
    on Oct 17, 2026
  4. EtaCassiopeia commented on Oct 17, 2026

    @EtaCassiopeia
    CollaboratorAuthor

    Second amendment (from the triage of #323, which is closed as a duplicate of this issue): rift#1523 also affects the work planned here. Verified against rift-java @ e7fc2bc and rift v0.23.0.

    Items to add

    1. Adopt rift#1517: spawn-mode imposters inherit --host 127.0.0.1 #321's planned IT fails on 0.23.0 as written. SpawnHostInheritanceIT case 2 expects a .host("0.0.0.0") imposter on a default Rift.spawn() to answer on a non-loopback local IPv4 address.
      • A default spawn has localOnly=true.
      • A connection from this host to its own site-local address has that address as its peer, not loopback. It is therefore dropped at accept.
      • Fix: run case 2 on Rift.spawn(SpawnOptions.builder().localOnly(false).build()).
    2. Add the #1523 case to the same IT. On a default spawn, open a raw socket to the same .host("0.0.0.0") imposter on that site-local address. The connection is accepted and then closed with no response. Read it to EOF, or catch the IOException. Then assert numberOfRequests == 0 through the admin API. This fails on 0.22.0 (the request is served), so gate it with EngineVersion.atLeast(version, "0.23.0"). Use assumeTrue when there is no site-local address, as case 2 does.
    3. Give Builder.localOnly(boolean) Javadoc, and have the host Javadoc link to it:
      /**
       * The engine's {@code --local-only}, on by default: the admin API and {@code /metrics} bind
       * loopback, and from rift 0.23.0 (rift#1523) every imposter and the front door refuse
       * non-loopback clients at accept, as Mountebank's {@code --localOnly} does. A refused client
       * gets no response and is not recorded. Imposter binds do not change. The intercept listener
       * is not filtered. Turn it off for a client that reaches the host off loopback, such as a
       * Linux Docker bridge ({@code 172.17.0.1}); Docker Desktop delivers from loopback and is
       * still admitted.
       */
      Change the host Javadoc's "localOnly pins the admin API to loopback" to "pins the admin API to loopback and, from 0.23.0, refuses non-loopback imposter clients". Mirror both in sdk-api.md:187-189.
    4. Intercept carve-out in the docs. The container recipe should not tell intercept users to drop localOnly. docs/intercept.md:365-375 (a container that proxies through host.docker.internal) keeps working under the default, because the intercept listener is unfiltered. Add one sentence there.

    Tests

    These are the IT changes from items 1–2: rift-java-conformance/.../SpawnHostInheritanceIT.java, SPAWN lane, RIFT_IT=1. No unit tests are needed: argv does not change (RiftProcessTest.java:27, :38).

    Docs / release notes

    Items 3–4 above. The release note for #321 gains: "localOnly (default on) now also refuses non-loopback clients on imposters (engine rift#1523); a Linux container needs localOnly(false)".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-to-implementTriaged: root cause confirmed, design and implementation steps recorded

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions