Skip to content

[Bubblewrap] Resolve DNS when the host uses a loopback stub resolver - #1453

Open
Soham Das (SohamDas2021) wants to merge 2 commits into
mainfrom
sohamdas2021-bwrap-resolver-pin-loopback-dns
Open

Soham Das (SohamDas2021) wants to merge 2 commits into
mainfrom
sohamdas2021-bwrap-resolver-pin-loopback-dns

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

On hosts that resolve through a loopback nameserver — systemd-resolved's 127.0.0.53 (the default on Ubuntu and Fedora), a local dnsmasq, Pi-hole — the Bubblewrap sandbox could not resolve any hostname. The sandbox runs in its own network namespace, so that address is its own empty loopback rather than the host's resolver. Connections to numeric addresses kept working, which is why every existing network test passed.

Under address filtering, the sandbox is now pointed at slirp's built-in DNS forwarder 10.0.2.3. libslirp rewrites a query sent there to the host's real nameserver and sends it from the host's network namespace, where the loopback resolver is reachable. search, options, and every other directive are carried over unchanged.

The replacement only applies when the host's own resolvers are all loopback and at least one is IPv4, so a host that resolves today is untouched, and it never applies when libslirp would have no IPv4 nameserver to forward to. The generated file is mounted over the path the /etc/resolv.conf symlink chain ends at, resolved through every component — bwrap cannot create a mount point beneath an unresolved symlink and aborts the sandbox when asked to.

The pin never costs you a sandbox. It improves on a resolver the sandbox already cannot reach, so every failure gives up the pin rather than the run: an unreadable resolver, a chain that cannot be resolved, a path the filesystem policy denies, and a failure to write the replacement all leave the sandbox exactly as it would have started without this feature, each with a warning naming the cause.

Proxy mode and ruleless-deny are deliberately unchanged: a proxied chain opens no port 53 and the proxy resolves, and an isolated sandbox has no connectivity to resolve with.

Limitations

  • Under egress.default: "deny" with rules, the chain still governs: a query to 10.0.2.3 is dropped unless a rule admits it. This was already true before this change. MXC now warns at launch when it pins the resolver and the chain admits nothing to 10.0.2.3:53, naming the rule to add rather than opening the port on the caller's behalf.
  • slirp's forwarder reaches the host's loopback resolver by design, and the chain's -d 10.0.2.2/32 -j DROP does not cover it. Name resolution is the one host-loopback service an address-filtered sandbox can reach. Documented under Loopback resolvers.
  • A resolver path can still change between being resolved and being mounted. The pin fails open if that happens, but the window is not closed.

🔗 References

Resolves github/copilot-cli#5027

🔍 Validation

New E2E suite tests/scripts/run_bwrap_dns_test.sh, registered in run_bwrap_all_tests.sh. It builds the loopback-resolver condition rather than requiring it — re-executing in a private user/mount/network namespace, serving DNS on 127.0.0.53, and pointing the resolver at it — so it behaves the same on a developer box and on a runner. It asserts a hostname resolves inside the sandbox, with a host-side control first so a sandbox failure is attributable. Verified it catches the bug: exit 1 against a binary built without the fix, exit 0 with it.

The reported failure and the two sandbox-aborting regressions found while reviewing it were each reproduced against the real lxc-exec before and after the fix.

  • From src/, cargo fmt --all -- --check: passed.
  • cargo clippy -p mxc-sdk --all-targets -- -D warnings: passed on Windows and x86_64-unknown-linux-gnu.
  • Ubuntu WSL, cargo test -p mxc-sdk --lib: 1981 passed. Two failures in mxc_pty and sdk_v1_conformance reproduce identically at this branch's merge-base and are unrelated.
  • cargo test -p mxc-sdk --lib bubblewrap::common: 342 passed.
  • 12 of the 16 Bubblewrap E2E suites pass locally. The four that exercise deniedPaths masking need passwordless sudo, which this host does not provide; CI covers them.

✅ Checklist

  • Signed the Contributor License Agreement
  • Linked to an issue
  • Updated documentation (if applicable)
  • Updated Copilot instructions (if build, architecture, or conventions changed)
  • If this PR changes Cargo.lock, the dependency-feed-check check passes

📋 Issue Type

  • Bug fix
  • Feature
  • Task
Microsoft Reviewers: Open in CodeFlow

… resolves through a loopback stub

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b7decf7b-a659-437e-a056-d3269346bb89
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b7decf7b-a659-437e-a056-d3269346bb89
Copilot AI balanced review requested due to automatic review settings October 8, 2026 19:42
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner October 8, 2026 19:42
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

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.

🟡 Changes recommended

Resolver fallback can abort or fail silently on non-UTF-8 paths, and the UDP-only firewall guidance omits required TCP fallback.

2 open findings
What changed in this PR

Fixes Bubblewrap DNS resolution when hosts use loopback stub resolvers by routing sandbox DNS through slirp.

Changes:

  • Adds loopback resolver detection, pinning, and firewall diagnostics.
  • Extracts shared symlink resolution logic.
  • Adds documentation, unit tests, and an end-to-end DNS test.
File Description
tests/​scripts/​run_bwrap_dns_test.sh Tests loopback-stub DNS resolution.
tests/​scripts/​run_bwrap_all_tests.sh Registers the DNS test.
tests/​configs/​bubblewrap_network_dns_stub.json Provides the E2E policy fixture.
src/​mxc-sdk/​src/​core/​mxc_common/​mod.rs Exposes the symlink helper.
src/​mxc-sdk/​src/​core/​mxc_common/​filesystem_symlink.rs Implements shared symlink resolution.
src/​mxc-sdk/​src/​backends/​bubblewrap/​common/​proxy_network.rs Detects, stages, and mounts resolver pins.
src/​mxc-sdk/​src/​backends/​bubblewrap/​common/​network_rules.rs Evaluates DNS-forwarder firewall admission.
src/​mxc-sdk/​src/​backends/​bubblewrap/​common/​bwrap_runner.rs Integrates resolver pinning and warnings.
src/​mxc-sdk/​src/​backends/​bubblewrap/​common/​bwrap_command.rs Clarifies resolver mount behavior.
docs/​backends/​bwrap/​bubblewrap-backend.md Documents loopback resolver handling.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

resolver: Option<&proxy_network::ResolverPin>,
logger: &mut Logger,
) {
if resolver.is_none() || plan.admits_udp(proxy_network::SLIRP_DNS_FORWARDER_IP, DNS_PORT) {
}
};

let destination_text = destination.to_str()?.to_string();

This branch has not been deployed

No deployments
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.

DNS broken for Linux Sandbox when using systemd-resolved stub resolver

2 participants