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 upto 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 intercepts traffic at L3 via TUN mode, essentially configuring the system routing table. This means:
- 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
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
- Private DNS: I run a private DNS server on my home network at 192.168.188.140, responsible for resolving internal services under .jinmiaoluo.com. When a domain has no private record, the DNS server automatically falls back to public DNS resolution
- Company internal services use .company.com domains, resolving to private IPs in 172.31.0.0/16, accessible only through VPN
SSH Security Policy
All SSH login requests to public servers are routed through the proxy server by default, providing a single egress IP.
All publicly exposed SSH services are protected by firewall rules that restrict SSH port access to only the proxy server’s egress IP. Benefits:
- Completely hidden SSH services: SSH ports are invisible to the internet; port scanners cannot discover them
- Eliminated brute-force risk: Attackers cannot attempt password or key brute-forcing
- Simplified key management: Even with weak passwords or old keys, the attack surface is blocked at the network layer
The sing-box configuration below is based on these policies.
Setup
- Install sing-box via Homebrew
brew install sing-box
- The sing-box configuration with sensitive values removed is shown below. Add your own values accordingly.
{
"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"
},
{
"rule_set": "geosite-cn",
"action": "route",
"server": "china-dns"
},
{
"query_type": ["A", "AAAA"],
"action": "route",
"server": "fakeip-dns"
},
{
"action": "route",
"server": "proxy-dns"
}
],
"strategy": "ipv4_only",
"independent_cache": true
},
"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.0.0/16"
],
"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,
"stack": "mixed"
}
],
"outbounds": [
{
"tag": "proxy",
"type": "vless",
"server": "xxx",
"server_port": 443,
"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.0.0/16"
],
"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 },
{ "network": "udp", "port": 443 },
{ "protocol": "stun" }
],
"action": "reject"
},
{
"rule_set": [
"geosite-google"
],
"outbound": "proxy"
},
{
"ip_is_private": false,
"port": 22,
"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
}
}
}
- 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
- (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.
- 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
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 will be officially removed in 1.13.0; see Migration):
inbound.sniffis deprecated, migrated to"action": "sniff"in route rulesinbound.sniff_override_destinationis 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
About Using 192.168.0.0/16 in VPN Routing Rules
In the configuration, the home network’s actual subnet is 192.168.188.0/24, but the VPN routing rule uses the broader 192.168.0.0/16:
"ip_cidr": [
"10.66.0.0/16",
"192.168.0.0/16"
],
"outbound": "home"
This design leverages the routing prefix tree (longest prefix match) priority mechanism:
- When away from home: accessing 192.168.188.x matches the 192.168.0.0/16 rule, routing traffic through VPN back home
- When connected to home WiFi: the system adds a local route for 192.168.188.0/24
- 192.168.188.0/24 is more specific than 192.168.0.0/16 (24 VS 16), giving it higher priority
- Result: the local route takes effect automatically, overriding the VPN routing rule, so LAN access no longer detours through VPN
This avoids the problem of LAN traffic still routing through VPN when at home.
About DNS Configuration with Mobile Hotspot
When a Mac connects to the internet via iPhone hotspot, the system automatically uses the DNS assigned by the iPhone. This causes an issue:
- Query
nas.jinmiaoluo.com - The carrier’s DNS has no private record, returning a public IP (or NXDOMAIN)
- 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, forcing all DNS queries through sing-box.
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:
- Application initiates a DNS query targeting 172.19.0.1
- Traffic enters the TUN interface and is intercepted by the
hijack-dnsrule - The sing-box DNS module routes by rule: .jinmiaoluo.com goes to private DNS, others are handled per rules
- Private DNS requests are sent through the WireGuard VPN, returning the correct private IP
About Blocking DoT, QUIC, and STUN
The configuration includes a routing rule that blocks three types of traffic:
{
"type": "logical",
"mode": "or",
"rules": [
{ "port": 853 },
{ "network": "udp", "port": 443 },
{ "protocol": "stun" }
],
"action": "reject"
}
Why block these three types of traffic:
- Port 853 (DNS over TLS): Some applications (e.g., Firefox) have built-in DoT/DoH clients that bypass system DNS and send encrypted queries directly to public DNS servers. These queries do not pass through sing-box’s DNS hijack rules, causing private domain resolution failures and FakeIP mechanism breakdown. After blocking port 853, applications fall back to plain DNS (port 53), which is properly captured by the
hijack-dnsrule - UDP 443 (QUIC): QUIC has its own congestion control. When traffic passes through the proxy chain (VLESS over TLS), QUIC’s congestion control interferes with the proxy chain’s TCP congestion control, reducing throughput. After blocking QUIC, browsers automatically fall back to HTTP/2 over TCP, where the proxy chain has only one layer of congestion control, resulting in better performance
- STUN protocol (WebRTC): Browsers use STUN for NAT traversal to discover the machine’s public IP. If STUN requests bypass the proxy and connect directly, the real IP address is exposed. Blocking STUN prevents WebRTC IP leaks
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 serviceswith no manual operation required in daily use
January 25, 2026, late night, at 1200bookshop on Beijing Road.