Skip to content

Multi-interface support: interface-bound providers and detection groups (#248) - #305

Open
timothymiller wants to merge 4 commits into
fix/reliability-2.2.1from
feat/multi-interface
Open

timothymiller wants to merge 4 commits into
fix/reliability-2.2.1from
feat/multi-interface

Conversation

@timothymiller

Copy link
Copy Markdown
Owner

Implements the RFC in #248. Stacked on #303 (base is fix/reliability-2.2.1); retarget to master after #303 merges.

Closes #248.

Interface-bound providers

cloudflare.trace, cloudflare.doh, ipify and url: accept an @<iface> suffix (IP4_PROVIDER=cloudflare.trace@ens5). The detection request is bound to that interface via reqwest::ClientBuilder::interface (SO_BINDTODEVICE on Linux/Android, IP_BOUND_IF on Apple/Solaris/illumos), or via a source-address bind elsewhere. No new crates are needed.

  • The suffix is split on the last @, and only when the suffix is a valid interface name. URLs with @ elsewhere (e.g. in the path) keep working.
  • A local provider with @iface is an error that points to local.iface:.
  • A bound provider that fails is a transient failure: records are preserved.

Detection groups

DOMAINS_N, IP4_DOMAINS_N, IP6_DOMAINS_N, IP4_PROVIDER_N and IP6_PROVIDER_N (N ≥ 2) add groups. The unsuffixed variables are the default group, so behavior is unchanged when no numbered variables are set.

  • A family with no provider in a group inherits the default group's provider. none disables the family in that group.
  • A group without domains is ignored with a warning. _1/_01 are rejected.
  • A (domain, record type) pair in two groups is a startup error.
  • Identical provider/family pairs are detected once per cycle.
  • WAF lists get the union of all groups' addresses and are left unchanged if any group's detection fails.
  • DELETE_ON_STOP covers every group. Legacy mode is not extended.

Open questions resolved as proposed

  1. Syntax: @iface suffix.
  2. Windows: source-address fallback, documented. It errors if the interface has no address of the family.
  3. Numbering starts at _2. _1 is rejected with a message rather than aliased.

Docs

docs/multi-interface.md. After #304 merges, link it from the README configuration section and add a CHANGELOG entry.

Testing

cargo test: 421 passing. New tests:

  • parsing, including ambiguous URLs
  • non-HTTP binding errors
  • a real socket test: a request bound to loopback succeeds and one bound to a missing interface fails
  • a bound-provider failure is classified as transient
  • group parsing and inheritance, _1 rejection, overlap rejection
  • env-mode detection from DOMAINS_2 alone
  • two groups writing different IPs to different records
  • shared detection across groups (exactly one request)

Not verified locally: the non-Unix (local_address) path, because only the macOS target is installed. CI only builds Linux.

HTTP providers (cloudflare.trace, cloudflare.doh, ipify, url:) accept an
@<interface> suffix that binds the detection request to that interface:
SO_BINDTODEVICE on Linux/Android, IP_BOUND_IF on Apple/Solaris/illumos, and
a source-address bind elsewhere. Part of #248.
DOMAINS_N / IP4_DOMAINS_N / IP6_DOMAINS_N with IP4_PROVIDER_N / IP6_PROVIDER_N
(N >= 2) define extra detection groups. Each group detects its own addresses
and updates its own domains; a family without a group provider inherits the
default group's. Identical provider/family pairs are detected once per cycle,
WAF lists receive the union of all groups' addresses, and a record configured
in two groups is a startup error. Without numbered variables behavior is
unchanged.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 04:42

Copilot AI 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.

🟡 Changes recommended

Disabled families can still have DNS records deleted, and ignored groups may incorrectly fail startup.

3 open findings
What changed in this PR

Adds multi-interface IP detection and per-domain detection groups.

Changes:

  • Adds interface-bound HTTP providers.
  • Adds numbered detection groups with provider inheritance and caching.
  • Updates DNS/WAF handling, tests, and documentation.
File Description
src/​provider.rs Implements interface binding and provider parsing.
src/​config.rs Parses and validates detection groups.
src/​updater.rs Updates records and WAF lists per group.
docs/​multi-interface.md Documents configuration and platform behavior.

🧠 Review effort: Balanced


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

Comment thread src/config.rs
Comment on lines +485 to +486
if !matches!(provider, ProviderType::None) {
providers.insert(ip_type, provider);
Comment thread src/config.rs
Comment on lines +473 to +496
let domains = read_domains_with_suffix(&suffix);

let mut providers = HashMap::new();
for (ip_type, var) in [(IpType::V4, "IP4_PROVIDER"), (IpType::V6, "IP6_PROVIDER")] {
let key = format!("{var}{suffix}");
let provider = match getenv(&key) {
Some(s) => ProviderType::parse(&s).map_err(|e| format!("Invalid {key}: {e}"))?,
None => match default_providers.get(&ip_type) {
Some(p) => p.clone(),
None => ProviderType::None,
},
};
if !matches!(provider, ProviderType::None) {
providers.insert(ip_type, provider);
}
}

if domains.is_empty() {
ppfmt.warningf(
pp::EMOJI_WARNING,
&format!("Detection group {n} has no DOMAINS{suffix}, IP4_DOMAINS{suffix} or IP6_DOMAINS{suffix}; ignoring it"),
);
continue;
}
Comment thread src/provider.rs
Comment on lines +415 to +419
fn bind_interface(
builder: reqwest::ClientBuilder,
iface: &str,
ip_type: IpType,
) -> Result<reqwest::ClientBuilder, String> {
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