Skip to content

feat(reachability): add domains and operator proxies to setup - #206

Closed
Rinse12 wants to merge 11 commits into
masterfrom
feat/reachability-domain
Closed

Rinse12 wants to merge 11 commits into
masterfrom
feat/reachability-domain

Conversation

@Rinse12

@Rinse12 Rinse12 commented Nov 2, 2026

Copy link
Copy Markdown
Member

Part of #198 (step 6). Based on #205, so earlier commits show here until those merge.

Summary

--domain <name>
  name resolves to one of our public IPs?  no -> refuse, showing both sides (--force overrides)
  443 free -> our Caddy gets a 90-day certificate, announce /dns4/<name>/tcp/443/tls/ws
  443 busy -> kubo gets its loopback ws listener, print a snippet for the detected server
              (nginx, Apache, Caddy; nginx as the example when unknown)

--announce <multiaddr>
  must end in /tls/ws or /wss
  kubo listeners applied first, because a proxy has nothing to forward to until then
  dial <multiaddr>/p2p/<our peer id>; only a completed handshake gets it announced

Everything that succeeded is announced together, and mode records the combination.

Decisions the issue left open:

  • A name with only AAAA records is announced as /dns6, because /dns4 could never connect.
  • --domain with 443 busy under --non-interactive exits 2 after printing the snippet, because the operator still has to act.
  • A stored domain that has gone stale warns and sits out the run, so it can't block repairing the IP path. It is refused only when passed explicitly on this run.

Evidence

  • Before: IP certificates only.
    After: npm run test:cli passes.
--domain does-not-exist.invalid        -> exit 1, shows the mismatch
--force --domain node.test             -> mode ip+domain, certificates for both, both announced
rerun, stored name now stale           -> exit 0, warning, IP still announced
--announce /ip4/.../tcp/4001           -> exit 1, not browser-dialable
port busy, --domain, non-interactive   -> exit 2, snippet names kubo's real ws port
--announce through a closed port       -> exit 1, nothing announced
--announce through a real proxy        -> handshake through an independent Caddy, mode reverse-proxy
rerun without the flag                 -> proxy address kept

Merge Danger

Door: two-way

Blast Radius: opt-in

Only runs when an operator passes these flags or answers the prompt. Like #205, this should not ship without step 7.

loadKuboConfigFile looked in .ipfs-bitsocial-cli while kubo runs in
.bitsocial-cli.ipfs or $IPFS_PATH, so the read always came back empty and the
daemon fell back to env vars and defaults. Both now use resolveKuboRepoPath.

That makes the multiaddr parsers reachable for the first time, so they now
handle IPv6 hosts, list-valued addresses and unix sockets.
computeKuboConfig(kuboConfig, reachabilityConfig, enabled) is the one place that
decides the kubo edits. planKuboConfig also returns a record of what changed,
which tells our entries from identical operator ones and lets disable restore
the touched fields byte for byte.
An explicit KUBO_RPC_URL or IPFS_GATEWAY_URL can name a kubo other than the one in
this repo, so it still wins over the kubo config. Connectivity checks now strip the
brackets a URL keeps around an IPv6 host.
reachability/config.json holds the operator's choices and refuses to load a
newer version rather than drop its fields. The daemon's check results go in a
separate file so they cannot overwrite a setup in progress.
…e status

Caddy keeps certificates for old addresses and other CAs until they expire, so
status now filters to the configured CA and current names. A damaged runtime
status file reads as no checks yet, and the text output prints domain and Caddy
checks.
…le tests

Caddy is downloaded on first use and checked against a sha256 pinned in source.
The generated Caddyfile keeps Caddy off UDP 443 and port 80. The integration test
issues a certificate for 127.0.0.1 from Pebble and completes a js-libp2p handshake
with kubo through Caddy.
Discovers public addresses from interfaces, kubo and the router, obtains an IP
certificate, applies the kubo config and restarts kubo. It checks what kubo really
listens on, because kubo stays silent when it cannot bind UDP 443.
--domain gets a normal certificate when 443 is free, or prints a proxy snippet for
the detected server when it is busy. --announce is added only after a libp2p
handshake through it succeeds. A name that does not resolve here is refused unless
--force is passed.
@coderabbitai

coderabbitai Bot commented Nov 2, 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 30 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
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9fd86edf-4d84-4435-9dc8-06192cb5f241
Commits

Reviewing files that changed from the base of the PR and between 6121b54 and ff035eb.

Files ignored due to path filters: 3
  • package-lock.json is excluded by !**/package-lock.json
  • test/fixtures/pebble/listener-cert.pem is excluded by !**/*.pem
  • test/fixtures/pebble/listener-key.pem is excluded by !**/*.pem
Files selected for processing: 26
  • README.md
  • package.json
  • src/cli/commands/daemon.ts
  • src/cli/commands/reachability/setup.ts
  • src/cli/commands/reachability/status.ts
  • src/ipfs/startIpfs.ts
  • src/reachability/caddy.ts
  • src/reachability/discovery.ts
  • src/reachability/guidance.ts
  • src/reachability/kubo-config.ts
  • src/reachability/libp2p-dial.ts
  • src/reachability/nat.ts
  • src/reachability/setup.ts
  • src/reachability/state.ts
  • src/util.ts
  • test/cli/daemon-honours-kubo-config-addresses.test.ts
  • test/cli/reachability-setup.integration.test.ts
  • test/cli/reachability-status.test.ts
  • test/helpers/pebble.ts
  • test/kubo/kubo-repo-path.test.ts
  • test/reachability/caddy-pebble.integration.test.ts
  • test/reachability/caddy.test.ts
  • test/reachability/discovery.test.ts
  • test/reachability/kubo-config.test.ts
  • test/reachability/setup.test.ts
  • test/reachability/state.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 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-11-02T13:26:35.594467Z ff035eb PR opened
ℹ️ 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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff035ebff4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/reachability/setup.ts
Comment on lines +230 to +232
if (!exitedEarly) {
await stopCaddy(spec.adminPort).catch(() => caddy.kill());
await exited;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep Caddy serving after certificate issuance

When setup starts its own Caddy—the normal first-run path—this block stops it immediately after certificate issuance, and no daemon code in this commit starts Caddy again (the only startCaddy call is in this function). Setup nevertheless records and announces /tls/ws addresses, so after the command exits TCP 443 is closed and browser/libp2p WSS dials fail; keep Caddy alive or have the daemon take it over before announcing the addresses.

Useful? React with 👍 / 👎.

Comment thread src/reachability/nat.ts

export async function discoverNatRouter(timeoutMs = 5000): Promise<NatRouter | undefined> {
const { upnpNat } = await import("@achingbrain/nat-port-mapper");
const client = upnpNat({ description: "bitsocial reachability", autoRefresh: false, ttl: MAPPING_TTL_MS });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refresh the UPnP mappings before their TTL expires

For nodes behind a UPnP router, these mappings are created with a one-hour TTL while autoRefresh is disabled, and neither setup nor the daemon renews them. Such a node can initially obtain certificates and report success but loses TCP/UDP reachability when the router expires the leases, so the mappings need automatic refresh or daemon-managed renewal.

Useful? React with 👍 / 👎.

Comment thread src/reachability/setup.ts
const message = dnsMismatchMessage(domain, [...records.v4, ...records.v6], publicIps);
// Refuse what the operator asked for now. A stored name going stale must not block repairing
// the rest, so it only sits out this run, and is still stored for when its DNS is fixed.
if (options.domain && !options.force) throw new SetupRefusedError(message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A domain typed at the interactive prompt (line 311) only sets the local domain, so options.domain is still undefined and this refusal is skipped. A mistyped answer is then treated like a stale stored domain: it warns, sits out the run, and is still saved to config.json (line 376). When publicIps.length === 0 the prompt is also shown (line 307), domainPointsHere is always false, so every answer is dropped but saved. The comment above says a domain the operator asked for now should be refused, and a prompt answer counts as that. Track whether the domain came from the flag or the prompt and use that here, and don't prompt when there are no public IPs to compare against.

Comment thread src/reachability/setup.ts
}

deps.log("WebTransport on UDP 443 is configured. No WSS address is announced.");
if (portBusy && domain) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This branch doesn't check domainUsable. With a stored domain whose DNS has gone stale (so domainUsable = false), TCP 443 busy, and --non-interactive or no TTY, setup prints a proxy snippet for a name it just warned doesn't point here, then throws NeedsAnswerError (exit 2). That contradicts the intent that a stale stored domain sits out the run and doesn't block automated repair.

Suggested change
if (portBusy && domain) {
if (portBusy && domain && domainUsable) {

Comment thread src/reachability/setup.ts
Comment on lines +137 to +144
export function deriveMode(parts: { ip: boolean; domain: boolean; proxy: boolean }): ReachabilityMode {
if (parts.ip && parts.domain) return "ip+domain";
if (parts.ip && parts.proxy) return "ip+reverse-proxy";
if (parts.ip) return "ip";
if (parts.domain) return "domain";
if (parts.proxy) return "reverse-proxy";
return "webtransport-only";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

deriveMode checks domain before proxy, and ReachabilityMode has no domain+proxy combination. When Caddy issues a domain certificate and a verified --announce proxy address is also present (the command's own example passes both), the stored mode is domain or ip+domain. reachability status prints Mode: domain while listing the proxy address, even though the PR says mode records the combination. Add modes for the combined cases, or derive the mode so the proxy part isn't lost. The deriveMode table in test/reachability/setup.test.ts also has no domain+proxy case.

@Rinse12

Rinse12 commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

Consolidated into #208.

@Rinse12 Rinse12 closed this Nov 2, 2026
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