I run mihomo in Docker on an N95 mini PC. macvlan gives the container its own LAN address (10.0.0.6), so any device that points its default gateway there gets rule-based proxying — without touching the router’s own routing table.
Two things bite you once the container is up: the host and its own macvlan containers cannot reach each other, and you still have to decide how clients switch over to the new gateway. This post covers both.
Why macvlan
services:
mihomo:
image: metacubex/mihomo:latest
container_name: mihomo
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./data:/root/.config/mihomo
networks:
macvlan_net:
ipv4_address: 10.0.0.6
# for IPv6, uncomment these and the two under sysctls
# driver_opts:
# com.docker.network.endpoint.sysctls: "net.ipv6.conf.IFNAME.disable_ipv6=0,net.ipv6.conf.IFNAME.accept_ra=2"
sysctls:
net.ipv4.ip_forward: 1
# net.ipv6.conf.all.disable_ipv6: 0
# net.ipv6.conf.all.forwarding: 1
networks:
macvlan_net:
driver: macvlan
driver_opts:
parent: enp1s0
ipam:
config:
- subnet: 10.0.0.0/24
gateway: 10.0.0.1
A bridge network hides the container behind the host’s NAT, so nothing else on the LAN can use it as a gateway. macvlan puts the container on the same layer 2 as every other device, with a real address of its own. That is the entire requirement here.
The commented lines are IPv6, and they really are optional: leave them commented and the container is v4-only. Uncomment them and it picks up a global address and a default route from the router’s RAs, surviving a dynamic prefix change without a config edit. Why IFNAME is a literal and why accept_ra has to be 2 (1 is silently ignored) is written up separately: Getting IPv6 into a macvlan Container.
Step 1: let the host and the container reach each other
A host cannot talk to containers on its own macvlan network, and they cannot talk back. This is a known limitation, and it is symmetric.
The reason is at the kernel level: the host’s physical NIC and its macvlan children share one underlying device, and the kernel will not loop packets between them. The container reaching other physical machines on the LAN works fine — that traffic goes out to the switch and comes back. Between the container and its own host there is simply no path.
The fix is to give the host its own macvlan sub-interface, usually called a shim:
ip link add macvlan-shim link enp1s0 type macvlan mode bridge
ip addr add 10.0.0.9/32 dev macvlan-shim # any free address in the subnet
ip link set macvlan-shim up
ip route add 10.0.0.6/32 dev macvlan-shim # host route to the mihomo container
Host traffic to 10.0.0.6 now goes out through the shim. The reverse direction comes for free: if the container needs a service running on the host (mine dials a SOCKS5 proxy on port 1080), anything published with docker run -p 1080:1080 binds 0.0.0.0, which includes the shim’s 10.0.0.9 — so 10.0.0.9:1080 works from inside the container.
To make it survive a reboot, a systemd oneshot:
# /etc/systemd/system/macvlan-shim.service
[Unit]
Description=macvlan shim for host-container communication
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ip link add macvlan-shim link enp1s0 type macvlan mode bridge
ExecStart=/sbin/ip addr add 10.0.0.9/32 dev macvlan-shim
ExecStart=/sbin/ip link set macvlan-shim up
ExecStart=/sbin/ip route add 10.0.0.6/32 dev macvlan-shim
ExecStop=/sbin/ip link del macvlan-shim
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now macvlan-shim.service
Step 2: get clients onto the gateway
Two ways to do this:
| Approach | What you change | Best for |
|---|---|---|
| Gateway + DNS per device | The client itself, or DHCP by MAC on the router | A stable set of devices you want proxied |
| A dedicated WiFi network | The router only (new subnet + SSID + policy routing) | Switching on demand; devices you’d rather not touch |
Option 1: point gateway and DNS at 10.0.0.6
Move DNS as well — the gateway alone is not enough. mihomo runs in fake-ip mode, which means it has to answer the client’s DNS queries in order to associate a domain with the connection that follows. Leave DNS pointed at the router and mihomo only ever sees a bare IP address, so domain rules (DOMAIN-SUFFIX, GEOSITE, and friends) never match and everything falls through to the default policy.
Turn IPv6 off on these devices too. If your ISP hands out public IPv6, the client will prefer v6 and go straight out, and the proxy might as well not be there.
By hand, one device at a time
Change the default gateway from the router (10.0.0.1) to 10.0.0.6 and put 10.0.0.6 in the DNS field. Fine for testing a machine or two, but it means a static address on each one, which stops being fun quickly.
In bulk, with OpenWrt DHCP tags
dnsmasq can tag hosts by MAC address and hand each tag its own gateway and DNS, with no client-side configuration at all. Tag the devices you want proxied, then use dhcp-option-force to push option 3 (gateway) and option 6 (DNS) to that tag.
In /etc/config/dhcp:
config host
option name 'my-device'
option mac 'AA:BB:CC:DD:EE:FF'
option tag 'bypass'
option ip '10.0.0.100'
config tag 'bypass'
list dhcp_option '3,10.0.0.6'
list dhcp_option '6,10.0.0.6'
Or in plain dnsmasq.conf style:
dhcp-host=AA:BB:CC:DD:EE:FF,set:bypass,10.0.0.100
dhcp-option-force=tag:bypass,3,10.0.0.6
dhcp-option-force=tag:bypass,6,10.0.0.6
set:bypass labels the matching device; dhcp-option-force applies only to that label. Untagged devices keep the router’s defaults and the two groups never interfere. Adding a device to the proxied set is one MAC address.
Restart dnsmasq and let the client renew its lease (or wait for it to expire):
/etc/init.d/dnsmasq restart
Note that these clients sit on the same subnet as mihomo, so their DNS queries reach it directly over layer 2 and never pass through the router. That detail matters a lot for the next option.
Option 2: a dedicated WiFi network
Tagging by MAC is still static grouping: a device that wants the proxy today and a direct connection tomorrow means going back into the router and forcing another lease. The nicer arrangement is a separate subnet on the router bound to its own SSID, with the whole subnet policy-routed into mihomo. Join that WiFi and everything is proxied; switch back to the main SSID and nothing is. The client is never configured at all.
Because mihomo lives on a different subnet, you cannot simply hand out 10.0.0.6 as the gateway — ARP does not cross the boundary. It takes source-based policy routing on OpenWrt, plus disarming one well-hidden DNS hijack along the way. That is enough material for its own post:
→ One SSID, Everything Proxied: Routing an OpenWrt Subnet into mihomo
Wrap-up
macvlan plus a shim interface makes the container behave like a real appliance on the LAN, reachable from the host included. On the client side, choose by how often you expect to change your mind: DHCP tags when the set of proxied devices is stable, a dedicated SSID when it isn’t. Both can run at the same time.