Introduction

This post explains how to use sing-box on macOS to replace the combination of wireguard-tools + Xray + Proxifier, achieving global proxy, multiple VPN network coexistence, and auto-start on boot.

The old approach had several pain points:

  • Scattered tooling: Required three tools working together – Xray (proxy) + Proxifier (global proxy) + wireguard-tools (multiple VPNs)
  • Manual startup: Each boot required launching Proxifier separately and manually running sudo wg-quick up to join each VPN network
  • VM incompatibility: Proxifier is based on NetworkExtension, which cannot share the proxy with NAT-based macOS VMs (e.g., Tart-based VMs)
  • Official WireGuard limitations: The Mac App Store WireGuard client does not support connecting to multiple VPN networks simultaneously, making userspace WireGuard the only viable option for now

sing-box uses system routes to send traffic into TUN, then selects a proxy, direct connection, or WireGuard endpoint according to its own rules:

  • A single tool handles both global proxy and multiple WireGuard VPNs
  • NAT-based VMs automatically inherit the host’s proxy and VPN configuration
  • Auto-start on boot via brew services, with no manual intervention required

Environment

The DNS and routing policies in this post assume the user is in Asia. Adjust the DNS servers and routing rules for other regions.

Network Layout

I need to connect to two VPN networks simultaneously:

  • Home network: 192.168.188.0/24
  • Company network: 172.31.0.0/16

DNS Resolution Strategy

  • Home domains: The home DNS server at 192.168.188.140 resolves .jinmiaoluo.com; when no private record exists, it looks up the public record
  • Company domains: This example assumes public DNS can resolve .company.com to addresses in 172.31.0.0/16, while access still requires the VPN. If these records exist only on company DNS, configure that server and access it through the company endpoint
  • Other domains: China domains receive real addresses; other A/AAAA queries use FakeIP
  • Default DNS: dns.final explicitly selects proxy-dns for queries that match no rule, such as TXT and MX queries for other domains

Private-domain rules must precede the FakeIP rule so the VPN subnet rules can match the real destination IP.

SSH Security Policy

All SSH login requests to public servers use the proxy server by default, providing a single egress IP. Firewalls on servers exposing SSH to the internet restrict SSH access to the proxy server’s egress IP. Benefits:

  • Hidden SSH services: SSH ports are invisible to the internet and cannot be discovered by port scans
  • Eliminated brute-force risk: Attackers cannot attempt password or key brute-forcing
  • Improved security: If OpenSSH has a security vulnerability, attacks are blocked at the network layer

The sing-box configuration below is based on these policies.

Setup

  1. Install sing-box via Homebrew
brew install sing-box
  1. The sing-box configuration with sensitive values removed is shown below. Add your own values accordingly.

The proxy server domain is resolved through the directly reachable china-dns, avoiding a dependency on the proxy itself.

{
  "log": {
    "level": "error",
    "timestamp": true
  },
  "dns": {
    "servers": [
      {
        "tag": "local-dns",
        "type": "udp",
        "server": "192.168.188.140",
        "server_port": 53,
        "detour": "home"
      },
      {
        "tag": "china-dns",
        "type": "https",
        "server": "223.5.5.5",
        "server_port": 443,
        "tls": {
          "server_name": "dns.alidns.com"
        }
      },
      {
        "tag": "proxy-dns",
        "type": "https",
        "server": "1.1.1.1",
        "detour": "proxy"
      },
      {
        "tag": "fakeip-dns",
        "type": "fakeip",
        "inet4_range": "198.18.0.0/15"
      }
    ],
    "rules": [
      {
        "domain_suffix": [".jinmiaoluo.com"],
        "action": "route",
        "server": "local-dns"
      },
      {
        "domain_suffix": [".company.com"],
        "action": "route",
        "server": "china-dns"
      },
      {
        "domain_suffix": [".bitwarden.com"],
        "action": "route",
        "server": "proxy-dns"
      },
      {
        "rule_set": "geosite-cn",
        "action": "route",
        "server": "china-dns"
      },
      {
        "query_type": ["A", "AAAA"],
        "action": "route",
        "server": "fakeip-dns"
      }
    ],
    "final": "proxy-dns",
    "strategy": "ipv4_only"
  },
  "endpoints": [
    {
      "tag": "home",
      "type": "wireguard",
      "private_key": "xxx",
      "address": [
        "10.66.153.175/16"
      ],
      "mtu": 1280,
      "peers": [
        {
          "public_key": "xxx",
          "pre_shared_key": "xxx",
          "address": "xxx",
          "port": 51820,
          "allowed_ips": [
            "10.66.0.0/16",
            "192.168.188.0/24"
          ],
          "persistent_keepalive_interval": 15
        }
      ]
    },
    {
      "tag": "company",
      "type": "wireguard",
      "private_key": "xxx",
      "address": ["10.99.128.68/16"],
      "mtu": 1280,
      "peers": [
        {
          "public_key": "xxx",
          "pre_shared_key": "xxx",
          "address": "xxx",
          "port": 51820,
          "allowed_ips": [
            "10.99.0.0/16",
            "172.31.0.0/16"
          ],
          "persistent_keepalive_interval": 15
        }
      ]
    }
  ],
  "inbounds": [
    {
      "tag": "tun-in",
      "type": "tun",
      "address": [
        "172.19.0.1/30"
      ],
      "mtu": 9000,
      "auto_route": true,
      "strict_route": true
    }
  ],
  "outbounds": [
    {
      "tag": "proxy",
      "type": "vless",
      "server": "xxx",
      "server_port": 443,
      "domain_resolver": "china-dns",
      "uuid": "xxx",
      "flow": "xtls-rprx-vision",
      "tls": {
        "enabled": true,
        "server_name": "proxy.example.com",
        "alpn": ["h2", "http/1.1"],
        "utls": {
          "enabled": true,
          "fingerprint": "chrome"
        }
      }
    },
    {
      "tag": "direct",
      "type": "direct"
    }
  ],
  "route": {
    "auto_detect_interface": true,
    "default_domain_resolver": "proxy-dns",
    "rules": [
      {
        "inbound": "tun-in",
        "action": "sniff",
        "timeout": "1s"
      },
      {
        "type": "logical",
        "mode": "or",
        "rules": [
          { "protocol": "dns" },
          { "port": 53 }
        ],
        "action": "hijack-dns"
      },
      {
        "rule_set": "geosite-category-ads-all",
        "action": "reject"
      },
      {
        "ip_cidr": [
          "10.66.0.0/16",
          "192.168.188.0/24"
        ],
        "outbound": "home"
      },
      {
        "ip_cidr": [
          "10.99.0.0/16",
          "172.31.0.0/16"
        ],
        "outbound": "company"
      },
      {
        "port": 123,
        "network": ["udp"],
        "outbound": "direct"
      },
      {
        "type": "logical",
        "mode": "or",
        "rules": [
          { "port": 853 },
          { "protocol": "stun" }
        ],
        "action": "reject"
      },
      {
        "rule_set": [
          "geosite-google"
        ],
        "outbound": "proxy"
      },
      {
        "type": "logical",
        "mode": "and",
        "rules": [
          { "port": 22 },
          { "ip_is_private": true, "invert": true }
        ],
        "action": "route",
        "outbound": "proxy"
      },
      {
        "rule_set": ["geosite-cn", "geosite-private"],
        "outbound": "direct"
      },
      {
        "rule_set": "geoip-cn",
        "outbound": "direct"
      },
      {
        "domain_suffix": [
          ".jinmiaoluo.com",
          ".company.com"
        ],
        "outbound": "direct"
      },
      {
        "ip_is_private": true,
        "outbound": "direct"
      }
    ],
    "rule_set": [
      {
        "tag": "geosite-cn",
        "type": "remote",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
        "download_detour": "proxy"
      },
      {
        "tag": "geosite-private",
        "type": "remote",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-private.srs",
        "download_detour": "proxy"
      },
      {
        "tag": "geosite-google",
        "type": "remote",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs",
        "download_detour": "proxy"
      },
      {
        "tag": "geosite-category-ads-all",
        "type": "remote",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs",
        "download_detour": "proxy"
      },
      {
        "tag": "geoip-cn",
        "type": "remote",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
        "download_detour": "proxy"
      }
    ],
    "final": "proxy"
  },
  "experimental": {
    "cache_file": {
      "enabled": true,
      "path": "cache.db",
      "store_fakeip": true
    }
  }
}
  1. Create the data directory and write the configuration file
mkdir -p /opt/homebrew/var/lib/sing-box
tee /opt/homebrew/etc/sing-box/config.json > /dev/null << 'EOF'
# Paste the JSON configuration above here
EOF
  1. (Optional) Debug the configuration

Before starting the service, you can verify the configuration by running sing-box manually:

sudo /opt/homebrew/opt/sing-box/bin/sing-box run \
    --config /opt/homebrew/etc/sing-box/config.json \
    --directory /opt/homebrew/var/lib/sing-box

When running manually, logs are printed directly to the terminal for easier troubleshooting. For more detailed logs, change log.level from error to debug in the configuration.

  1. Start the service
sudo brew services start sing-box

Once started, the global proxy + multiple VPN network setup is complete. The service will auto-start on subsequent boots.

Additional Notes

Matching Public SSH Traffic

ip_is_private: false disables the condition; it does not match public IPs. Use logical + and to combine port 22 with the private-IP condition, setting invert: true only in the private-IP sub-rule to express port == 22 && !ip_is_private.

About sniff

The route rule "action": "sniff" enables protocol sniffing on tun-in inbound traffic, extracting the real domain name from TLS SNI or HTTP Host headers for subsequent route rule matching. "timeout": "1s" specifies the sniffing timeout; if exceeded, sniffing is abandoned and routing continues.

The sniff configuration underwent a major change in sing-box 1.11.0 (old fields were removed in 1.13.0; see Migration):

  • inbound.sniff is deprecated, migrated to "action": "sniff" in route rules
  • inbound.sniff_override_destination is deprecated and removed with no equivalent replacement. The developer considers “replacing destination IP with the sniffed domain” an incorrect design inherited from V2Ray (reference). The new sniff only extracts domains for route matching without replacing the destination IP. This aligns perfectly with our needs – after private domains are resolved to private IPs via local-dns, the destination address always remains that private IP

System Routes and VPN Routing

System routes determine whether traffic enters TUN; once it does, route.rules selects an outbound in rule order. ip_cidr does not itself install system routes for those subnets.

The home subnet is 192.168.188.0/24, matching WireGuard allowed_ips. Direct LAN access at home depends on system routes; local-dns specifies detour: home, so its upstream queries still use the home WireGuard endpoint.

Vaultwarden Push and FakeIP

If the Vaultwarden server’s DNS queries pass through this sing-box instance, FakeIP may trigger its non-public address check, causing push requests to fail. The configuration sends .bitwarden.com queries to proxy-dns before the FakeIP rule, returning real addresses while retaining Vaultwarden’s security check.

About DNS Configuration with Mobile Hotspot

When a Mac connects to the internet via iPhone hotspot, the system uses the DNS assigned by the iPhone. If queries do not enter sing-box, the following issue may occur:

  1. Query nas.jinmiaoluo.com
  2. The carrier’s DNS has no private record, returning a public IP (or NXDOMAIN)
  3. The private DNS server’s (192.168.188.140) records cannot take effect

Although sing-box is configured with a hijack-dns rule to intercept DNS traffic, macOS may bypass the TUN interface and query the system DNS directly in certain scenarios.

Solution: Manually set the system DNS to the TUN interface address so applications using system DNS resolve through sing-box. Built-in DoH requires separate configuration.

Path: System Settings > Wi-Fi > Click Details next to the connected network > DNS > Add 172.19.0.1

After configuration, the DNS query flow becomes:

  1. Application initiates a DNS query targeting 172.19.0.1
  2. Traffic enters the TUN interface and is intercepted by the hijack-dns rule
  3. The sing-box DNS module routes by rule: .jinmiaoluo.com goes to private DNS, others are handled per rules
  4. Private DNS requests are sent through the WireGuard VPN, returning the correct private IP

Restore the previous DNS settings before stopping sing-box to avoid querying the unavailable 172.19.0.1.

About Blocking DoT and STUN

The configuration includes a routing rule that blocks two types of traffic:

{
  "type": "logical",
  "mode": "or",
  "rules": [
    { "port": 853 },
    { "protocol": "stun" }
  ],
  "action": "reject"
}

Why block these two types of traffic:

  • Port 853: Blocks DoT/DoQ on this port, but not DoH, which usually uses port 443. Fallback to plain DNS depends on the client’s settings
  • STUN protocol (WebRTC): Prevents requests that reach this rule and are identified as STUN from discovering a public address, but may disrupt calls or connection establishment. It does not guarantee protection against all WebRTC address leaks

IPv6 proxying requires server support. The current setup is IPv4-only and does not yet support IPv6. ipv4_only does not change system IPv6 settings.

Open System Settings > Network, select the active network service, then go to Details > TCP/IP. Record the current settings, change Configure IPv6 to Link-local only, and click OK.

This retains IPv6 communication on the local link. To restore the previous configuration, reapply the settings you recorded.

Summary

This is the most complete solution I have found for macOS (26.2) so far. It solves the following problems simultaneously:

  • Global proxy: All traffic passes through the TUN interface with no per-application proxy configuration needed
  • Multiple VPN coexistence: Simultaneously connected to home and company WireGuard VPNs with automatic traffic splitting by destination IP, resolving routing conflicts
  • Fully automated: After initial setup, auto-starts on boot via brew services with no manual operation required in daily use

January 25, 2026, late night, at 1200bookshop on Beijing Road.