Question and planning around Mycelium as system-wide dns resolver #3

Open
opened 2026-01-19 11:10:18 +00:00 by delandtj · 0 comments
Owner

In order to get Mycelium to be used as a system resolver for all platforms, we'll need to see how we can do that:

  • IOS/Android have hooks to do just that
  • How is it done in windows/OSX
  • for linux, there are many different solutions, let's enumerate them.

From CLaude:

# Mycelium DNS Integration Guide

## Overview

This document describes how Mycelium integrates with system DNS across all supported platforms to resolve `.mycelium` names (and optionally all DNS) through the Mycelium network.

### Operating Modes

| Mode | Behavior |
|------|----------|
| `split` | Only `*.mycelium` queries go through Mycelium resolvers; all other queries use original system DNS |
| `full` | All DNS queries routed through Mycelium DNS forwarders |

### Architecture

```text
┌─────────────────────────────────────────────────────────────┐
│                      Application                            │
│                  queries "foo.mycelium"                     │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                Mycelium DNS Stub (local)                    │
│                     127.0.0.1:53                            │
│                                                             │
│     dns_mode: split                 dns_mode: full          │
│     ┌───────────┬──────────┐       ┌──────────────────┐     │
│     │*.mycelium │  other   │       │   ALL queries    │     │
│     └─────┬─────┴────┬─────┘       └────────┬─────────┘     │
│           │          │                      │               │
│           ▼          ▼                      ▼               │
│     ┌──────────┐ ┌──────────┐       ┌──────────────────┐    │
│     │ Mycelium │ │  System  │       │  Mycelium DNS    │    │
│     │ resolver │ │ upstream │       │   forwarders     │    │
│     │400::/53  │ │(captured)│       │  (400::xxx/53)   │    │
│     └──────────┘ └──────────┘       └──────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Configuration

[dns]
enabled = true
mode = "split"  # "split" or "full"
listen = "127.0.0.1:53"

# Mycelium-internal DNS forwarders (well-known or discovered)
mycelium_resolvers = [
    "400::1:53",
    "400::2:53",
]

# Only used in "full" mode for non-.mycelium queries
# If empty, uses mycelium_resolvers for everything
full_mode_forwarders = []

Component 1: DNS Stub Resolver

This component is shared across all platforms. It's a lightweight DNS server embedded in the Mycelium daemon.

Responsibilities

  1. Listen on 127.0.0.1:53 (UDP and TCP)
  2. Parse incoming DNS queries
  3. Route based on mode and query name
  4. Forward to appropriate upstream and relay response

Rust Crate Recommendations

Crate Purpose
hickory-dns (formerly trust-dns) DNS protocol parsing, server, and client
hickory-resolver High-level resolver for upstream queries
socket2 Low-level socket control (SO_REUSEADDR, binding)

Code Sketch

use hickory_server::server::{ServerFuture, RequestHandler, ResponseHandler};
use hickory_proto::op::{Header, ResponseCode, Message};
use std::net::SocketAddr;

pub struct MyceliumDnsHandler {
    mode: DnsMode,
    mycelium_resolvers: Vec<SocketAddr>,  // 400::x addresses
    system_upstream: Option<SocketAddr>,   // Captured at startup
}

#[derive(Clone, Copy)]
pub enum DnsMode {
    Split,
    Full,
}

impl MyceliumDnsHandler {
    fn should_use_mycelium(&self, query_name: &str) -> bool {
        match self.mode {
            DnsMode::Full => true,
            DnsMode::Split => query_name.ends_with(".mycelium.") 
                           || query_name == "mycelium.",
        }
    }
    
    async fn forward_query(&self, query: &Message, upstream: SocketAddr) -> Result<Message, Error> {
        // Create UDP socket, send query, await response
        // Handle timeout, retry with TCP if truncated
        todo!()
    }
}

impl RequestHandler for MyceliumDnsHandler {
    async fn handle_request<R: ResponseHandler>(
        &self,
        request: &Request,
        response_handle: R,
    ) -> Result<(), Error> {
        let query = request.message.queries().first().unwrap();
        let name = query.name().to_string();
        
        let upstream = if self.should_use_mycelium(&name) {
            self.mycelium_resolvers.first().copied()
        } else {
            self.system_upstream
        };
        
        match upstream {
            Some(addr) => {
                let response = self.forward_query(request.message, addr).await?;
                response_handle.send_response(response).await
            }
            None => {
                // No upstream available, return SERVFAIL
                let mut response = request.message.clone();
                response.set_response_code(ResponseCode::ServFail);
                response_handle.send_response(response).await
            }
        }
    }
}

pub async fn start_dns_server(handler: MyceliumDnsHandler) -> Result<(), Error> {
    let mut server = ServerFuture::new(handler);
    
    let socket = UdpSocket::bind("127.0.0.1:53").await?;
    server.register_socket(socket);
    
    let listener = TcpListener::bind("127.0.0.1:53").await?;
    server.register_listener(listener, Duration::from_secs(30));
    
    server.block_until_done().await
}

Binding to Port 53

Port 53 is privileged (< 1024). Options:

Platform Solution
Linux CAP_NET_BIND_SERVICE capability, or start as root then drop privileges
macOS Run as root, or use launchd socket activation
Windows Run as admin or as a service (services can bind low ports)
Mobile VPN tunnel handles this - no direct binding needed

Component 2: System DNS Capture

Before hijacking DNS, we need to know where queries were going so we can forward non-Mycelium queries there (in split mode).

Linux

Multiple sources, in order of preference:

use std::fs;
use std::net::IpAddr;

pub fn capture_linux_dns() -> Vec<IpAddr> {
    // 1. Try systemd-resolved D-Bus API (most accurate on modern systems)
    if let Ok(servers) = capture_from_resolved_dbus() {
        return servers;
    }
    
    // 2. Try /run/systemd/resolve/resolv.conf (resolved's upstream view)
    if let Ok(content) = fs::read_to_string("/run/systemd/resolve/resolv.conf") {
        return parse_resolv_conf(&content);
    }
    
    // 3. Fall back to /etc/resolv.conf
    if let Ok(content) = fs::read_to_string("/etc/resolv.conf") {
        return parse_resolv_conf(&content);
    }
    
    vec![]
}

fn parse_resolv_conf(content: &str) -> Vec<IpAddr> {
    content
        .lines()
        .filter_map(|line| {
            let line = line.trim();
            if line.starts_with("nameserver ") {
                line[11..].trim().parse().ok()
            } else {
                None
            }
        })
        .collect()
}

fn capture_from_resolved_dbus() -> Result<Vec<IpAddr>, Error> {
    // Use zbus crate to query org.freedesktop.resolve1
    // Method: org.freedesktop.resolve1.Manager.GetLink / GetLinkDns
    todo!()
}

Edge cases:

  • 127.0.0.53 in resolv.conf means systemd-resolved is in use - dig deeper via D-Bus
  • NetworkManager may be managing DNS - check /etc/NetworkManager/conf.d/
  • Some systems use resolvconf which manages /etc/resolv.conf dynamically

macOS

use std::process::Command;

pub fn capture_macos_dns() -> Vec<IpAddr> {
    // scutil --dns gives detailed DNS config
    let output = Command::new("scutil")
        .arg("--dns")
        .output()
        .expect("failed to run scutil");
    
    parse_scutil_output(&String::from_utf8_lossy(&output.stdout))
}

fn parse_scutil_output(output: &str) -> Vec<IpAddr> {
    // Parse output like:
    // resolver #1
    //   nameserver[0] : 192.168.1.1
    //   nameserver[1] : 8.8.8.8
    
    let mut servers = vec![];
    for line in output.lines() {
        let line = line.trim();
        if line.starts_with("nameserver[") {
            if let Some(addr_str) = line.split(':').nth(1) {
                if let Ok(addr) = addr_str.trim().parse() {
                    servers.push(addr);
                }
            }
        }
    }
    servers
}

Alternative using SystemConfiguration framework:

// Using core-foundation and system-configuration crates
use system_configuration::dynamic_store::SCDynamicStoreBuilder;

pub fn capture_macos_dns_native() -> Vec<IpAddr> {
    let store = SCDynamicStoreBuilder::new("mycelium").build();
    
    // Key: "State:/Network/Global/DNS" contains current DNS servers
    if let Some(dict) = store.get("State:/Network/Global/DNS") {
        // Parse CFDictionary for "ServerAddresses" key
        todo!()
    }
    
    vec![]
}

Windows

use std::process::Command;

pub fn capture_windows_dns() -> Vec<IpAddr> {
    // PowerShell: Get-DnsClientServerAddress
    let output = Command::new("powershell")
        .args([
            "-Command",
            "Get-DnsClientServerAddress -AddressFamily IPv4 | \
             Where-Object { $_.ServerAddresses } | \
             Select-Object -ExpandProperty ServerAddresses"
        ])
        .output()
        .expect("failed to run powershell");
    
    String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter_map(|line| line.trim().parse().ok())
        .collect()
}

Native approach using Windows API:

// Using windows crate
use windows::Win32::NetworkManagement::IpHelper::*;

pub fn capture_windows_dns_native() -> Vec<IpAddr> {
    // GetAdaptersAddresses gives DNS servers per adapter
    // GetNetworkParams gives system-wide DNS
    todo!()
}

iOS & Android

On mobile, capture happens at VPN configuration time:

iOS: Query NEDNSSettings from current network configuration
Android: Use ConnectivityManager.getLinkProperties().getDnsServers()

Store these before applying Mycelium's VPN DNS settings.


Component 3: System DNS Hijack

Make the OS send DNS queries to our stub resolver.

Linux

Option A: Direct /etc/resolv.conf manipulation (simple but fragile)

use std::fs;
use std::os::unix::fs::PermissionsExt;

const RESOLV_CONF: &str = "/etc/resolv.conf";
const BACKUP_PATH: &str = "/etc/resolv.conf.mycelium-backup";

pub fn hijack_linux_direct() -> Result<(), Error> {
    // Backup original
    fs::copy(RESOLV_CONF, BACKUP_PATH)?;
    
    // Write our resolver
    fs::write(RESOLV_CONF, "# Managed by Mycelium\nnameserver 127.0.0.1\n")?;
    
    // Make immutable to prevent NetworkManager/dhcpcd overwriting
    // (requires root)
    Command::new("chattr").args(["+i", RESOLV_CONF]).status()?;
    
    Ok(())
}

pub fn restore_linux_direct() -> Result<(), Error> {
    // Remove immutable flag
    Command::new("chattr").args(["-i", RESOLV_CONF]).status()?;
    
    // Restore backup
    fs::copy(BACKUP_PATH, RESOLV_CONF)?;
    fs::remove_file(BACKUP_PATH)?;
    
    Ok(())
}

Option B: systemd-resolved integration (cleaner on systemd systems)

pub fn hijack_linux_resolved() -> Result<(), Error> {
    // Set DNS for the default route interface
    // resolvectl dns <interface> 127.0.0.1
    // resolvectl domain <interface> ~.  (catch-all)
    
    let interface = get_default_interface()?;
    
    Command::new("resolvectl")
        .args(["dns", &interface, "127.0.0.1"])
        .status()?;
    
    Command::new("resolvectl")
        .args(["domain", &interface, "~."])
        .status()?;
    
    Ok(())
}

fn get_default_interface() -> Result<String, Error> {
    // ip route show default | grep -oP 'dev \K\S+'
    let output = Command::new("ip")
        .args(["route", "show", "default"])
        .output()?;
    
    let stdout = String::from_utf8_lossy(&output.stdout);
    stdout
        .split_whitespace()
        .skip_while(|&s| s != "dev")
        .nth(1)
        .map(String::from)
        .ok_or_else(|| Error::NoDefaultRoute)
}

Option C: Split DNS only via resolved (cleanest for split mode)

pub fn hijack_linux_resolved_split() -> Result<(), Error> {
    let interface = get_default_interface()?;
    
    // Only route .mycelium to our resolver
    Command::new("resolvectl")
        .args(["dns", &interface, "127.0.0.1"])
        .status()?;
    
    // ~mycelium means "route .mycelium queries here"
    Command::new("resolvectl")
        .args(["domain", &interface, "~mycelium"])
        .status()?;
    
    Ok(())
}

Detection: Which method to use

pub enum LinuxDnsManager {
    Resolved,           // systemd-resolved active
    NetworkManager,     // NM without resolved
    Direct,             // Plain /etc/resolv.conf
}

pub fn detect_linux_dns_manager() -> LinuxDnsManager {
    // Check if systemd-resolved is running
    if Command::new("systemctl")
        .args(["is-active", "systemd-resolved"])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
    {
        return LinuxDnsManager::Resolved;
    }
    
    // Check if NetworkManager is managing DNS
    if Path::new("/etc/NetworkManager/NetworkManager.conf").exists() {
        return LinuxDnsManager::NetworkManager;
    }
    
    LinuxDnsManager::Direct
}

macOS

Option A: Per-TLD resolver (split mode only - RECOMMENDED)

use std::fs;

pub fn hijack_macos_split() -> Result<(), Error> {
    // macOS automatically routes *.mycelium to this resolver
    fs::create_dir_all("/etc/resolver")?;
    fs::write(
        "/etc/resolver/mycelium",
        "nameserver 127.0.0.1\nport 53\n"
    )?;
    
    Ok(())
}

pub fn restore_macos_split() -> Result<(), Error> {
    fs::remove_file("/etc/resolver/mycelium")?;
    Ok(())
}

This is the cleanest approach - no system-wide DNS changes, works alongside existing DNS.

Option B: System-wide DNS override (full mode)

pub fn hijack_macos_full() -> Result<(), Error> {
    // Using networksetup (user-facing but works)
    // Get active network service first
    let service = get_active_network_service()?;
    
    Command::new("networksetup")
        .args(["-setdnsservers", &service, "127.0.0.1"])
        .status()?;
    
    Ok(())
}

pub fn restore_macos_full(original_servers: &[IpAddr]) -> Result<(), Error> {
    let service = get_active_network_service()?;
    
    let servers: Vec<String> = if original_servers.is_empty() {
        vec!["empty".to_string()]  // "empty" clears DNS, reverts to DHCP
    } else {
        original_servers.iter().map(|s| s.to_string()).collect()
    };
    
    let mut args = vec!["-setdnsservers".to_string(), service];
    args.extend(servers);
    
    Command::new("networksetup")
        .args(&args)
        .status()?;
    
    Ok(())
}

fn get_active_network_service() -> Result<String, Error> {
    // networksetup -listallnetworkservices, find active one
    // Or use scutil --nwi to get primary interface
    
    let output = Command::new("networksetup")
        .args(["-listallnetworkservices"])
        .output()?;
    
    // Usually "Wi-Fi" or "Ethernet" - pick first non-asterisk line
    String::from_utf8_lossy(&output.stdout)
        .lines()
        .skip(1)  // Skip header
        .find(|line| !line.starts_with('*'))
        .map(String::from)
        .ok_or(Error::NoActiveNetwork)
}

Option C: Using scutil (more programmatic)

pub fn hijack_macos_scutil() -> Result<(), Error> {
    // Create a temporary file with the configuration
    let config = r#"
d.init
d.add ServerAddresses * 127.0.0.1
set State:/Network/Service/mycelium/DNS
"#;
    
    let mut child = Command::new("scutil")
        .stdin(Stdio::piped())
        .spawn()?;
    
    child.stdin.take().unwrap().write_all(config.as_bytes())?;
    child.wait()?;
    
    Ok(())
}

Windows

Option A: netsh (simple, built-in)

pub fn hijack_windows_netsh() -> Result<(), Error> {
    // Find active interface
    let interface = get_active_interface_windows()?;
    
    Command::new("netsh")
        .args([
            "interface", "ip", "set", "dns",
            &format!("name={}", interface),
            "static", "127.0.0.1"
        ])
        .status()?;
    
    Ok(())
}

pub fn restore_windows_netsh(interface: &str) -> Result<(), Error> {
    // Revert to DHCP
    Command::new("netsh")
        .args([
            "interface", "ip", "set", "dns",
            &format!("name={}", interface),
            "dhcp"
        ])
        .status()?;
    
    Ok(())
}

Option B: PowerShell (more robust)

pub fn hijack_windows_powershell() -> Result<(), Error> {
    Command::new("powershell")
        .args([
            "-Command",
            r#"
            $adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' }
            foreach ($adapter in $adapters) {
                Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ServerAddresses '127.0.0.1'
            }
            "#
        ])
        .status()?;
    
    Ok(())
}

Option C: NRPT for split DNS (Windows 7+, RECOMMENDED for split mode)

Name Resolution Policy Table allows routing specific domains to specific servers:

pub fn hijack_windows_nrpt_split() -> Result<(), Error> {
    // PowerShell to add NRPT rule
    Command::new("powershell")
        .args([
            "-Command",
            r#"
            Add-DnsClientNrptRule -Namespace ".mycelium" -NameServers "127.0.0.1"
            "#
        ])
        .status()?;
    
    Ok(())
}

pub fn restore_windows_nrpt_split() -> Result<(), Error> {
    Command::new("powershell")
        .args([
            "-Command",
            r#"
            Get-DnsClientNrptRule | Where-Object { $_.Namespace -eq '.mycelium' } | Remove-DnsClientNrptRule -Force
            "#
        ])
        .status()?;
    
    Ok(())
}

Registry-based NRPT (persistent, survives reboot):

pub fn hijack_windows_nrpt_registry() -> Result<(), Error> {
    // HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\<GUID>
    // Create key with:
    //   Name: .mycelium
    //   GenericDNSServers: 127.0.0.1
    //   ConfigOptions: 0x8
    
    Command::new("reg")
        .args([
            "add",
            r"HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\MyceliumDNS",
            "/v", "Name",
            "/t", "REG_MULTI_SZ",
            "/d", ".mycelium",
            "/f"
        ])
        .status()?;
    
    Command::new("reg")
        .args([
            "add",
            r"HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\MyceliumDNS",
            "/v", "GenericDNSServers",
            "/t", "REG_SZ",
            "/d", "127.0.0.1",
            "/f"
        ])
        .status()?;
    
    // Flush DNS cache to apply
    Command::new("ipconfig").args(["/flushdns"]).status()?;
    
    Ok(())
}

iOS

Using Network Extension framework:

import NetworkExtension

class MyceliumTunnelProvider: NEPacketTunnelProvider {
    
    override func startTunnel(options: [String : NSObject]?, 
                               completionHandler: @escaping (Error?) -> Void) {
        
        let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "10.0.0.1")
        
        // DNS configuration
        let dnsSettings = NEDNSSettings(servers: ["10.0.0.1"])  // Tunnel-internal resolver
        
        // Split mode: only .mycelium
        dnsSettings.matchDomains = ["mycelium"]
        
        // Full mode: all DNS
        // dnsSettings.matchDomains = [""]  // Empty string = match all
        
        // Prevent DNS leaks
        dnsSettings.matchDomainsNoSearch = true
        
        settings.dnsSettings = dnsSettings
        
        setTunnelNetworkSettings(settings) { error in
            completionHandler(error)
        }
    }
}

Key considerations:

  • matchDomains = ["mycelium"] → only .mycelium queries go through VPN DNS
  • matchDomains = [""] → ALL queries go through VPN DNS
  • Resolver must listen on a tunnel-internal IP (e.g., 10.0.0.1)
  • Cannot use 127.0.0.1 from within VPN - traffic wouldn't route correctly

Android

Using VpnService:

class MyceliumVpnService : VpnService() {
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val builder = Builder()
        
        // Tunnel configuration
        builder.addAddress("10.0.0.2", 24)
        
        // DNS server (Mycelium internal resolver)
        builder.addDnsServer("10.0.0.1")
        
        // For split mode, only route specific subnets
        // Android doesn't support per-domain DNS routing natively,
        // so we handle it in our resolver
        
        // Mycelium IPv6 range
        builder.addRoute("400::", 7)
        
        // For full DNS mode, add this:
        // builder.addRoute("0.0.0.0", 0)  // All IPv4
        // builder.addRoute("::", 0)        // All IPv6
        
        val interface = builder.establish()
        
        return START_STICKY
    }
}

Split DNS on Android:

Android VpnService doesn't support per-domain DNS like iOS. Workarounds:

  1. Handle in resolver: Route all DNS through Mycelium resolver, which then forwards non-Mycelium queries to original system DNS

  2. Route only port 53: Only capture DNS traffic:

builder.addRoute("10.0.0.1", 32)  // Our resolver
// Don't add default route - only Mycelium traffic goes through tunnel
  1. Android 10+ Private DNS: Can register as a DoT server, but complex

Component 4: Lifecycle Management

Startup Sequence

pub async fn start_dns_service(config: &DnsConfig) -> Result<DnsService, Error> {
    // 1. Capture current system DNS (before we change anything)
    let original_dns = capture_system_dns().await?;
    
    // 2. Start stub resolver
    let handler = MyceliumDnsHandler::new(
        config.mode,
        config.mycelium_resolvers.clone(),
        original_dns.first().copied(),  // For split mode forwarding
    );
    let server_handle = start_dns_server(handler).await?;
    
    // 3. Hijack system DNS
    hijack_system_dns(config.mode).await?;
    
    Ok(DnsService {
        server_handle,
        original_dns,
    })
}

Shutdown Sequence

impl DnsService {
    pub async fn stop(self) -> Result<(), Error> {
        // 1. Restore original DNS FIRST
        // (so system has working DNS even if we crash)
        restore_system_dns(&self.original_dns).await?;
        
        // 2. Stop stub resolver
        self.server_handle.shutdown().await?;
        
        Ok(())
    }
}

Crash Recovery

Store state to allow recovery after crash:

const STATE_FILE: &str = "/var/lib/mycelium/dns-state.json";

#[derive(Serialize, Deserialize)]
struct DnsState {
    active: bool,
    mode: DnsMode,
    original_dns: Vec<IpAddr>,
    hijack_method: String,
}

pub fn persist_state(state: &DnsState) -> Result<(), Error> {
    fs::write(STATE_FILE, serde_json::to_string(state)?)?;
    Ok(())
}

pub fn recover_on_startup() -> Result<(), Error> {
    if let Ok(content) = fs::read_to_string(STATE_FILE) {
        let state: DnsState = serde_json::from_str(&content)?;
        
        if state.active {
            // Previous session didn't shut down cleanly
            // Restore original DNS
            restore_system_dns(&state.original_dns)?;
        }
    }
    
    // Clear state file
    let _ = fs::remove_file(STATE_FILE);
    
    Ok(())
}

Security Considerations

Port 53 Binding

  • Only bind to 127.0.0.1:53, never 0.0.0.0:53
  • Otherwise you become an open resolver (DDoS amplification vector)

DNS Cache Poisoning

  • Validate responses match queries (transaction ID, question section)
  • Consider implementing DNSSEC validation for upstream queries
  • Use random source ports for upstream queries

Privilege Management

// Linux: Drop privileges after binding port 53
pub fn drop_privileges(uid: u32, gid: u32) -> Result<(), Error> {
    // Must be done AFTER binding socket but BEFORE handling queries
    nix::unistd::setgid(Gid::from_raw(gid))?;
    nix::unistd::setuid(Uid::from_raw(uid))?;
    Ok(())
}

DNS Leak Prevention

In full mode, ensure DNS doesn't leak via:

  1. Disable WebRTC DNS - Browser setting, not controllable by Mycelium
  2. Block port 53 outbound - Firewall rule to prevent apps bypassing resolver:
    # Linux iptables example
    iptables -A OUTPUT -p udp --dport 53 ! -d 127.0.0.1 -j DROP
    iptables -A OUTPUT -p tcp --dport 53 ! -d 127.0.0.1 -j DROP
    
  3. DoH/DoT blocking - More complex, apps can bypass via encrypted DNS

Testing

Manual Testing

# Check stub is listening
dig @127.0.0.1 test.mycelium

# Check system DNS is hijacked
dig google.com  # Should go through stub

# Check Mycelium resolution
dig @127.0.0.1 somenode.mycelium

# Trace DNS path
dig +trace google.com

Integration Tests

#[tokio::test]
async fn test_split_mode_routing() {
    let service = start_test_dns_service(DnsMode::Split).await;
    
    // .mycelium should go to Mycelium resolver
    let response = query(&service, "test.mycelium").await;
    assert!(response.source == Source::Mycelium);
    
    // Other domains should go to system upstream
    let response = query(&service, "google.com").await;
    assert!(response.source == Source::SystemUpstream);
}

#[tokio::test]
async fn test_full_mode_routing() {
    let service = start_test_dns_service(DnsMode::Full).await;
    
    // Everything should go to Mycelium forwarders
    let response = query(&service, "google.com").await;
    assert!(response.source == Source::MyceliumForwarder);
}

Platform Summary

Platform Split Mode Approach Full Mode Approach Notes
Linux systemd-resolved ~mycelium domain resolv.conf or resolved ~. Detect DNS manager first
macOS /etc/resolver/mycelium networksetup -setdnsservers Per-TLD resolver is cleanest
Windows NRPT rule for .mycelium Set adapter DNS via PowerShell NRPT survives reboots
iOS matchDomains: ["mycelium"] matchDomains: [""] Via NEDNSSettings in tunnel
Android Handle in resolver addDnsServer + default route No native per-domain support

Appendix: Crate Dependencies

[dependencies]
hickory-dns = "0.24"           # DNS protocol and server
hickory-resolver = "0.24"      # Upstream query client
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Platform-specific
[target.'cfg(unix)'.dependencies]
nix = "0.27"                   # Unix system calls, privilege dropping

[target.'cfg(target_os = "linux")'.dependencies]
zbus = "4"                     # D-Bus for systemd-resolved

[target.'cfg(target_os = "macos")'.dependencies]
system-configuration = "0.5"   # macOS SCDynamicStore
core-foundation = "0.9"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.52", features = [
    "Win32_NetworkManagement_IpHelper",
    "Win32_Networking_WinSock"
]}
In order to get Mycelium to be used as a system resolver for all platforms, we'll need to see how we can do that: - IOS/Android have hooks to do just that - How is it done in windows/OSX - for linux, there are many different solutions, let's enumerate them. From CLaude: ``` # Mycelium DNS Integration Guide ## Overview This document describes how Mycelium integrates with system DNS across all supported platforms to resolve `.mycelium` names (and optionally all DNS) through the Mycelium network. ### Operating Modes | Mode | Behavior | |------|----------| | `split` | Only `*.mycelium` queries go through Mycelium resolvers; all other queries use original system DNS | | `full` | All DNS queries routed through Mycelium DNS forwarders | ### Architecture ```text ┌─────────────────────────────────────────────────────────────┐ │ Application │ │ queries "foo.mycelium" │ └─────────────────────────┬───────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Mycelium DNS Stub (local) │ │ 127.0.0.1:53 │ │ │ │ dns_mode: split dns_mode: full │ │ ┌───────────┬──────────┐ ┌──────────────────┐ │ │ │*.mycelium │ other │ │ ALL queries │ │ │ └─────┬─────┴────┬─────┘ └────────┬─────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ Mycelium │ │ System │ │ Mycelium DNS │ │ │ │ resolver │ │ upstream │ │ forwarders │ │ │ │400::/53 │ │(captured)│ │ (400::xxx/53) │ │ │ └──────────┘ └──────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Configuration ```toml [dns] enabled = true mode = "split" # "split" or "full" listen = "127.0.0.1:53" # Mycelium-internal DNS forwarders (well-known or discovered) mycelium_resolvers = [ "400::1:53", "400::2:53", ] # Only used in "full" mode for non-.mycelium queries # If empty, uses mycelium_resolvers for everything full_mode_forwarders = [] ``` --- ## Component 1: DNS Stub Resolver This component is shared across all platforms. It's a lightweight DNS server embedded in the Mycelium daemon. ### Responsibilities 1. Listen on `127.0.0.1:53` (UDP and TCP) 2. Parse incoming DNS queries 3. Route based on mode and query name 4. Forward to appropriate upstream and relay response ### Rust Crate Recommendations | Crate | Purpose | |-------|---------| | `hickory-dns` (formerly trust-dns) | DNS protocol parsing, server, and client | | `hickory-resolver` | High-level resolver for upstream queries | | `socket2` | Low-level socket control (SO_REUSEADDR, binding) | ### Code Sketch ```rust use hickory_server::server::{ServerFuture, RequestHandler, ResponseHandler}; use hickory_proto::op::{Header, ResponseCode, Message}; use std::net::SocketAddr; pub struct MyceliumDnsHandler { mode: DnsMode, mycelium_resolvers: Vec<SocketAddr>, // 400::x addresses system_upstream: Option<SocketAddr>, // Captured at startup } #[derive(Clone, Copy)] pub enum DnsMode { Split, Full, } impl MyceliumDnsHandler { fn should_use_mycelium(&self, query_name: &str) -> bool { match self.mode { DnsMode::Full => true, DnsMode::Split => query_name.ends_with(".mycelium.") || query_name == "mycelium.", } } async fn forward_query(&self, query: &Message, upstream: SocketAddr) -> Result<Message, Error> { // Create UDP socket, send query, await response // Handle timeout, retry with TCP if truncated todo!() } } impl RequestHandler for MyceliumDnsHandler { async fn handle_request<R: ResponseHandler>( &self, request: &Request, response_handle: R, ) -> Result<(), Error> { let query = request.message.queries().first().unwrap(); let name = query.name().to_string(); let upstream = if self.should_use_mycelium(&name) { self.mycelium_resolvers.first().copied() } else { self.system_upstream }; match upstream { Some(addr) => { let response = self.forward_query(request.message, addr).await?; response_handle.send_response(response).await } None => { // No upstream available, return SERVFAIL let mut response = request.message.clone(); response.set_response_code(ResponseCode::ServFail); response_handle.send_response(response).await } } } } pub async fn start_dns_server(handler: MyceliumDnsHandler) -> Result<(), Error> { let mut server = ServerFuture::new(handler); let socket = UdpSocket::bind("127.0.0.1:53").await?; server.register_socket(socket); let listener = TcpListener::bind("127.0.0.1:53").await?; server.register_listener(listener, Duration::from_secs(30)); server.block_until_done().await } ``` ### Binding to Port 53 Port 53 is privileged (< 1024). Options: | Platform | Solution | |----------|----------| | Linux | `CAP_NET_BIND_SERVICE` capability, or start as root then drop privileges | | macOS | Run as root, or use launchd socket activation | | Windows | Run as admin or as a service (services can bind low ports) | | Mobile | VPN tunnel handles this - no direct binding needed | --- ## Component 2: System DNS Capture Before hijacking DNS, we need to know where queries were going so we can forward non-Mycelium queries there (in split mode). ### Linux Multiple sources, in order of preference: ```rust use std::fs; use std::net::IpAddr; pub fn capture_linux_dns() -> Vec<IpAddr> { // 1. Try systemd-resolved D-Bus API (most accurate on modern systems) if let Ok(servers) = capture_from_resolved_dbus() { return servers; } // 2. Try /run/systemd/resolve/resolv.conf (resolved's upstream view) if let Ok(content) = fs::read_to_string("/run/systemd/resolve/resolv.conf") { return parse_resolv_conf(&content); } // 3. Fall back to /etc/resolv.conf if let Ok(content) = fs::read_to_string("/etc/resolv.conf") { return parse_resolv_conf(&content); } vec![] } fn parse_resolv_conf(content: &str) -> Vec<IpAddr> { content .lines() .filter_map(|line| { let line = line.trim(); if line.starts_with("nameserver ") { line[11..].trim().parse().ok() } else { None } }) .collect() } fn capture_from_resolved_dbus() -> Result<Vec<IpAddr>, Error> { // Use zbus crate to query org.freedesktop.resolve1 // Method: org.freedesktop.resolve1.Manager.GetLink / GetLinkDns todo!() } ``` **Edge cases:** - `127.0.0.53` in resolv.conf means systemd-resolved is in use - dig deeper via D-Bus - NetworkManager may be managing DNS - check `/etc/NetworkManager/conf.d/` - Some systems use `resolvconf` which manages `/etc/resolv.conf` dynamically ### macOS ```rust use std::process::Command; pub fn capture_macos_dns() -> Vec<IpAddr> { // scutil --dns gives detailed DNS config let output = Command::new("scutil") .arg("--dns") .output() .expect("failed to run scutil"); parse_scutil_output(&String::from_utf8_lossy(&output.stdout)) } fn parse_scutil_output(output: &str) -> Vec<IpAddr> { // Parse output like: // resolver #1 // nameserver[0] : 192.168.1.1 // nameserver[1] : 8.8.8.8 let mut servers = vec![]; for line in output.lines() { let line = line.trim(); if line.starts_with("nameserver[") { if let Some(addr_str) = line.split(':').nth(1) { if let Ok(addr) = addr_str.trim().parse() { servers.push(addr); } } } } servers } ``` **Alternative using SystemConfiguration framework:** ```rust // Using core-foundation and system-configuration crates use system_configuration::dynamic_store::SCDynamicStoreBuilder; pub fn capture_macos_dns_native() -> Vec<IpAddr> { let store = SCDynamicStoreBuilder::new("mycelium").build(); // Key: "State:/Network/Global/DNS" contains current DNS servers if let Some(dict) = store.get("State:/Network/Global/DNS") { // Parse CFDictionary for "ServerAddresses" key todo!() } vec![] } ``` ### Windows ```rust use std::process::Command; pub fn capture_windows_dns() -> Vec<IpAddr> { // PowerShell: Get-DnsClientServerAddress let output = Command::new("powershell") .args([ "-Command", "Get-DnsClientServerAddress -AddressFamily IPv4 | \ Where-Object { $_.ServerAddresses } | \ Select-Object -ExpandProperty ServerAddresses" ]) .output() .expect("failed to run powershell"); String::from_utf8_lossy(&output.stdout) .lines() .filter_map(|line| line.trim().parse().ok()) .collect() } ``` **Native approach using Windows API:** ```rust // Using windows crate use windows::Win32::NetworkManagement::IpHelper::*; pub fn capture_windows_dns_native() -> Vec<IpAddr> { // GetAdaptersAddresses gives DNS servers per adapter // GetNetworkParams gives system-wide DNS todo!() } ``` ### iOS & Android On mobile, capture happens at VPN configuration time: **iOS:** Query `NEDNSSettings` from current network configuration **Android:** Use `ConnectivityManager.getLinkProperties().getDnsServers()` Store these before applying Mycelium's VPN DNS settings. --- ## Component 3: System DNS Hijack Make the OS send DNS queries to our stub resolver. ### Linux **Option A: Direct /etc/resolv.conf manipulation (simple but fragile)** ```rust use std::fs; use std::os::unix::fs::PermissionsExt; const RESOLV_CONF: &str = "/etc/resolv.conf"; const BACKUP_PATH: &str = "/etc/resolv.conf.mycelium-backup"; pub fn hijack_linux_direct() -> Result<(), Error> { // Backup original fs::copy(RESOLV_CONF, BACKUP_PATH)?; // Write our resolver fs::write(RESOLV_CONF, "# Managed by Mycelium\nnameserver 127.0.0.1\n")?; // Make immutable to prevent NetworkManager/dhcpcd overwriting // (requires root) Command::new("chattr").args(["+i", RESOLV_CONF]).status()?; Ok(()) } pub fn restore_linux_direct() -> Result<(), Error> { // Remove immutable flag Command::new("chattr").args(["-i", RESOLV_CONF]).status()?; // Restore backup fs::copy(BACKUP_PATH, RESOLV_CONF)?; fs::remove_file(BACKUP_PATH)?; Ok(()) } ``` **Option B: systemd-resolved integration (cleaner on systemd systems)** ```rust pub fn hijack_linux_resolved() -> Result<(), Error> { // Set DNS for the default route interface // resolvectl dns <interface> 127.0.0.1 // resolvectl domain <interface> ~. (catch-all) let interface = get_default_interface()?; Command::new("resolvectl") .args(["dns", &interface, "127.0.0.1"]) .status()?; Command::new("resolvectl") .args(["domain", &interface, "~."]) .status()?; Ok(()) } fn get_default_interface() -> Result<String, Error> { // ip route show default | grep -oP 'dev \K\S+' let output = Command::new("ip") .args(["route", "show", "default"]) .output()?; let stdout = String::from_utf8_lossy(&output.stdout); stdout .split_whitespace() .skip_while(|&s| s != "dev") .nth(1) .map(String::from) .ok_or_else(|| Error::NoDefaultRoute) } ``` **Option C: Split DNS only via resolved (cleanest for split mode)** ```rust pub fn hijack_linux_resolved_split() -> Result<(), Error> { let interface = get_default_interface()?; // Only route .mycelium to our resolver Command::new("resolvectl") .args(["dns", &interface, "127.0.0.1"]) .status()?; // ~mycelium means "route .mycelium queries here" Command::new("resolvectl") .args(["domain", &interface, "~mycelium"]) .status()?; Ok(()) } ``` **Detection: Which method to use** ```rust pub enum LinuxDnsManager { Resolved, // systemd-resolved active NetworkManager, // NM without resolved Direct, // Plain /etc/resolv.conf } pub fn detect_linux_dns_manager() -> LinuxDnsManager { // Check if systemd-resolved is running if Command::new("systemctl") .args(["is-active", "systemd-resolved"]) .status() .map(|s| s.success()) .unwrap_or(false) { return LinuxDnsManager::Resolved; } // Check if NetworkManager is managing DNS if Path::new("/etc/NetworkManager/NetworkManager.conf").exists() { return LinuxDnsManager::NetworkManager; } LinuxDnsManager::Direct } ``` ### macOS **Option A: Per-TLD resolver (split mode only - RECOMMENDED)** ```rust use std::fs; pub fn hijack_macos_split() -> Result<(), Error> { // macOS automatically routes *.mycelium to this resolver fs::create_dir_all("/etc/resolver")?; fs::write( "/etc/resolver/mycelium", "nameserver 127.0.0.1\nport 53\n" )?; Ok(()) } pub fn restore_macos_split() -> Result<(), Error> { fs::remove_file("/etc/resolver/mycelium")?; Ok(()) } ``` This is the cleanest approach - no system-wide DNS changes, works alongside existing DNS. **Option B: System-wide DNS override (full mode)** ```rust pub fn hijack_macos_full() -> Result<(), Error> { // Using networksetup (user-facing but works) // Get active network service first let service = get_active_network_service()?; Command::new("networksetup") .args(["-setdnsservers", &service, "127.0.0.1"]) .status()?; Ok(()) } pub fn restore_macos_full(original_servers: &[IpAddr]) -> Result<(), Error> { let service = get_active_network_service()?; let servers: Vec<String> = if original_servers.is_empty() { vec!["empty".to_string()] // "empty" clears DNS, reverts to DHCP } else { original_servers.iter().map(|s| s.to_string()).collect() }; let mut args = vec!["-setdnsservers".to_string(), service]; args.extend(servers); Command::new("networksetup") .args(&args) .status()?; Ok(()) } fn get_active_network_service() -> Result<String, Error> { // networksetup -listallnetworkservices, find active one // Or use scutil --nwi to get primary interface let output = Command::new("networksetup") .args(["-listallnetworkservices"]) .output()?; // Usually "Wi-Fi" or "Ethernet" - pick first non-asterisk line String::from_utf8_lossy(&output.stdout) .lines() .skip(1) // Skip header .find(|line| !line.starts_with('*')) .map(String::from) .ok_or(Error::NoActiveNetwork) } ``` **Option C: Using scutil (more programmatic)** ```rust pub fn hijack_macos_scutil() -> Result<(), Error> { // Create a temporary file with the configuration let config = r#" d.init d.add ServerAddresses * 127.0.0.1 set State:/Network/Service/mycelium/DNS "#; let mut child = Command::new("scutil") .stdin(Stdio::piped()) .spawn()?; child.stdin.take().unwrap().write_all(config.as_bytes())?; child.wait()?; Ok(()) } ``` ### Windows **Option A: netsh (simple, built-in)** ```rust pub fn hijack_windows_netsh() -> Result<(), Error> { // Find active interface let interface = get_active_interface_windows()?; Command::new("netsh") .args([ "interface", "ip", "set", "dns", &format!("name={}", interface), "static", "127.0.0.1" ]) .status()?; Ok(()) } pub fn restore_windows_netsh(interface: &str) -> Result<(), Error> { // Revert to DHCP Command::new("netsh") .args([ "interface", "ip", "set", "dns", &format!("name={}", interface), "dhcp" ]) .status()?; Ok(()) } ``` **Option B: PowerShell (more robust)** ```rust pub fn hijack_windows_powershell() -> Result<(), Error> { Command::new("powershell") .args([ "-Command", r#" $adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } foreach ($adapter in $adapters) { Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ServerAddresses '127.0.0.1' } "# ]) .status()?; Ok(()) } ``` **Option C: NRPT for split DNS (Windows 7+, RECOMMENDED for split mode)** Name Resolution Policy Table allows routing specific domains to specific servers: ```rust pub fn hijack_windows_nrpt_split() -> Result<(), Error> { // PowerShell to add NRPT rule Command::new("powershell") .args([ "-Command", r#" Add-DnsClientNrptRule -Namespace ".mycelium" -NameServers "127.0.0.1" "# ]) .status()?; Ok(()) } pub fn restore_windows_nrpt_split() -> Result<(), Error> { Command::new("powershell") .args([ "-Command", r#" Get-DnsClientNrptRule | Where-Object { $_.Namespace -eq '.mycelium' } | Remove-DnsClientNrptRule -Force "# ]) .status()?; Ok(()) } ``` **Registry-based NRPT (persistent, survives reboot):** ```rust pub fn hijack_windows_nrpt_registry() -> Result<(), Error> { // HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\<GUID> // Create key with: // Name: .mycelium // GenericDNSServers: 127.0.0.1 // ConfigOptions: 0x8 Command::new("reg") .args([ "add", r"HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\MyceliumDNS", "/v", "Name", "/t", "REG_MULTI_SZ", "/d", ".mycelium", "/f" ]) .status()?; Command::new("reg") .args([ "add", r"HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig\MyceliumDNS", "/v", "GenericDNSServers", "/t", "REG_SZ", "/d", "127.0.0.1", "/f" ]) .status()?; // Flush DNS cache to apply Command::new("ipconfig").args(["/flushdns"]).status()?; Ok(()) } ``` ### iOS Using Network Extension framework: ```swift import NetworkExtension class MyceliumTunnelProvider: NEPacketTunnelProvider { override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "10.0.0.1") // DNS configuration let dnsSettings = NEDNSSettings(servers: ["10.0.0.1"]) // Tunnel-internal resolver // Split mode: only .mycelium dnsSettings.matchDomains = ["mycelium"] // Full mode: all DNS // dnsSettings.matchDomains = [""] // Empty string = match all // Prevent DNS leaks dnsSettings.matchDomainsNoSearch = true settings.dnsSettings = dnsSettings setTunnelNetworkSettings(settings) { error in completionHandler(error) } } } ``` **Key considerations:** - `matchDomains = ["mycelium"]` → only `.mycelium` queries go through VPN DNS - `matchDomains = [""]` → ALL queries go through VPN DNS - Resolver must listen on a tunnel-internal IP (e.g., `10.0.0.1`) - Cannot use `127.0.0.1` from within VPN - traffic wouldn't route correctly ### Android Using VpnService: ```kotlin class MyceliumVpnService : VpnService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val builder = Builder() // Tunnel configuration builder.addAddress("10.0.0.2", 24) // DNS server (Mycelium internal resolver) builder.addDnsServer("10.0.0.1") // For split mode, only route specific subnets // Android doesn't support per-domain DNS routing natively, // so we handle it in our resolver // Mycelium IPv6 range builder.addRoute("400::", 7) // For full DNS mode, add this: // builder.addRoute("0.0.0.0", 0) // All IPv4 // builder.addRoute("::", 0) // All IPv6 val interface = builder.establish() return START_STICKY } } ``` **Split DNS on Android:** Android VpnService doesn't support per-domain DNS like iOS. Workarounds: 1. **Handle in resolver:** Route all DNS through Mycelium resolver, which then forwards non-Mycelium queries to original system DNS 2. **Route only port 53:** Only capture DNS traffic: ```kotlin builder.addRoute("10.0.0.1", 32) // Our resolver // Don't add default route - only Mycelium traffic goes through tunnel ``` 3. **Android 10+ Private DNS:** Can register as a DoT server, but complex --- ## Component 4: Lifecycle Management ### Startup Sequence ```rust pub async fn start_dns_service(config: &DnsConfig) -> Result<DnsService, Error> { // 1. Capture current system DNS (before we change anything) let original_dns = capture_system_dns().await?; // 2. Start stub resolver let handler = MyceliumDnsHandler::new( config.mode, config.mycelium_resolvers.clone(), original_dns.first().copied(), // For split mode forwarding ); let server_handle = start_dns_server(handler).await?; // 3. Hijack system DNS hijack_system_dns(config.mode).await?; Ok(DnsService { server_handle, original_dns, }) } ``` ### Shutdown Sequence ```rust impl DnsService { pub async fn stop(self) -> Result<(), Error> { // 1. Restore original DNS FIRST // (so system has working DNS even if we crash) restore_system_dns(&self.original_dns).await?; // 2. Stop stub resolver self.server_handle.shutdown().await?; Ok(()) } } ``` ### Crash Recovery Store state to allow recovery after crash: ```rust const STATE_FILE: &str = "/var/lib/mycelium/dns-state.json"; #[derive(Serialize, Deserialize)] struct DnsState { active: bool, mode: DnsMode, original_dns: Vec<IpAddr>, hijack_method: String, } pub fn persist_state(state: &DnsState) -> Result<(), Error> { fs::write(STATE_FILE, serde_json::to_string(state)?)?; Ok(()) } pub fn recover_on_startup() -> Result<(), Error> { if let Ok(content) = fs::read_to_string(STATE_FILE) { let state: DnsState = serde_json::from_str(&content)?; if state.active { // Previous session didn't shut down cleanly // Restore original DNS restore_system_dns(&state.original_dns)?; } } // Clear state file let _ = fs::remove_file(STATE_FILE); Ok(()) } ``` --- ## Security Considerations ### Port 53 Binding - Only bind to `127.0.0.1:53`, never `0.0.0.0:53` - Otherwise you become an open resolver (DDoS amplification vector) ### DNS Cache Poisoning - Validate responses match queries (transaction ID, question section) - Consider implementing DNSSEC validation for upstream queries - Use random source ports for upstream queries ### Privilege Management ```rust // Linux: Drop privileges after binding port 53 pub fn drop_privileges(uid: u32, gid: u32) -> Result<(), Error> { // Must be done AFTER binding socket but BEFORE handling queries nix::unistd::setgid(Gid::from_raw(gid))?; nix::unistd::setuid(Uid::from_raw(uid))?; Ok(()) } ``` ### DNS Leak Prevention In full mode, ensure DNS doesn't leak via: 1. **Disable WebRTC DNS** - Browser setting, not controllable by Mycelium 2. **Block port 53 outbound** - Firewall rule to prevent apps bypassing resolver: ```bash # Linux iptables example iptables -A OUTPUT -p udp --dport 53 ! -d 127.0.0.1 -j DROP iptables -A OUTPUT -p tcp --dport 53 ! -d 127.0.0.1 -j DROP ``` 3. **DoH/DoT blocking** - More complex, apps can bypass via encrypted DNS --- ## Testing ### Manual Testing ```bash # Check stub is listening dig @127.0.0.1 test.mycelium # Check system DNS is hijacked dig google.com # Should go through stub # Check Mycelium resolution dig @127.0.0.1 somenode.mycelium # Trace DNS path dig +trace google.com ``` ### Integration Tests ```rust #[tokio::test] async fn test_split_mode_routing() { let service = start_test_dns_service(DnsMode::Split).await; // .mycelium should go to Mycelium resolver let response = query(&service, "test.mycelium").await; assert!(response.source == Source::Mycelium); // Other domains should go to system upstream let response = query(&service, "google.com").await; assert!(response.source == Source::SystemUpstream); } #[tokio::test] async fn test_full_mode_routing() { let service = start_test_dns_service(DnsMode::Full).await; // Everything should go to Mycelium forwarders let response = query(&service, "google.com").await; assert!(response.source == Source::MyceliumForwarder); } ``` --- ## Platform Summary | Platform | Split Mode Approach | Full Mode Approach | Notes | |----------|--------------------|--------------------|-------| | Linux | systemd-resolved `~mycelium` domain | resolv.conf or resolved `~.` | Detect DNS manager first | | macOS | `/etc/resolver/mycelium` | `networksetup -setdnsservers` | Per-TLD resolver is cleanest | | Windows | NRPT rule for `.mycelium` | Set adapter DNS via PowerShell | NRPT survives reboots | | iOS | `matchDomains: ["mycelium"]` | `matchDomains: [""]` | Via NEDNSSettings in tunnel | | Android | Handle in resolver | `addDnsServer` + default route | No native per-domain support | --- ## Appendix: Crate Dependencies ```toml [dependencies] hickory-dns = "0.24" # DNS protocol and server hickory-resolver = "0.24" # Upstream query client tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" # Platform-specific [target.'cfg(unix)'.dependencies] nix = "0.27" # Unix system calls, privilege dropping [target.'cfg(target_os = "linux")'.dependencies] zbus = "4" # D-Bus for systemd-resolved [target.'cfg(target_os = "macos")'.dependencies] system-configuration = "0.5" # macOS SCDynamicStore core-foundation = "0.9" [target.'cfg(windows)'.dependencies] windows = { version = "0.52", features = [ "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock" ]} ```
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
geomind_code/mycelium_network_gui#3
No description provided.