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 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.140resolves.jinmiaoluo.com; when no private record exists, it looks up the public record - Company domains: This example assumes public DNS can resolve
.company.comto addresses in172.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 thecompanyendpoint - Other domains: China domains receive real addresses; other A/AAAA queries use FakeIP
- Default DNS:
dns.finalexplicitly selectsproxy-dnsfor 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
- 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.
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
}
}
}
- 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
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.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
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:
- 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 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:
- 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.comgoes to private DNS, others are handled per rules - 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
Link-local IPv6
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 serviceswith no manual operation required in daily use
January 25, 2026, late night, at 1200bookshop on Beijing Road.