English | Русский · Telegram · Code of Conduct
Multi-arch Docker container for MikroTik RouterOS: mihomo + byedpi + zapret + zapret2, fully ENV-driven, with a built-in sh-only WebUI that generates ready-to-paste RouterOS commands.
- 🌍 Multi-arch: ARM, ARM64, AMD64v1/v2/v3 (the
latesttag bundles ARM, ARM64, AMD64v3 — for v1/v2 pull the dedicated tag) - 🖥 Built-in WebUI on port
80— visual ENV editor, YAML validator, AWG/proxy/rule-set file managers, generates MikroTik terminal commands - 🔐 DPI bypass via ByeDPI, Zapret/nfqws, Zapret2/nfqws2 (nfqws/nfqws2 — amd64/arm64 only)
- 🧩 Flexible routing by domain / IP / GeoSite / GeoIP / ASN, all controlled via ENV
- 🛡 Multiple proxy links and subscriptions (including RemnaWave with HWID)
- 🚀 WireGuard / AmneziaWG integration by dropping
.conffiles into a mount folder - 📦 Single-step automated install via MikroTik terminal snippet
- 🛠 Multiple VETH interfaces appear as outbound proxies → mangle in RouterOS to send traffic where you want
Tested with RouterOS 7.20+. Requires the
containerpackage anddevice-mode container=yes.
-
Enable container support on RouterOS:
/system/device-mode/print /system/device-mode/update mode=advanced container=yes traffic-gen=yesYou have ~5 minutes to confirm via power-cycle or physical button.
-
Paste the install snippet into RouterOS terminal — see § RouterOS install below.
-
Open the WebUI:
http://<container-ip>:80/Configure ENVs visually, click MikroTik commands → copy → paste back into RouterOS terminal. -
Or use mihomo's panel:
http://<container-ip>:9090/(UI fromEXTERNAL_UI_URL).
http://<container-ip>:80/ — local management panel served by busybox httpd from the container itself.
It does not modify the running container directly. Instead it:
- shows every ENV that
entrypoint.shunderstands, grouped into logical pages (Core, Providers, DPI, Groups, Rules, Rule-sets, YAML, Tools) - tracks edits locally in
localStorageagainst the original values - generates the exact
/container/envs/add|set|removecommands you need to paste into RouterOS terminal, plus the final/container/stop+startto apply
What's also in there:
- Proxy YAML editor with 12 protocol templates (vless-tcp/reality, vless-xhttp, vmess, trojan, ss, anytls, wireguard, amneziawg, hysteria2, tuic, ssh, vless-ws) — "Load template" fills the textarea
- Live
mihomo -tvalidation of proxy YAMLs before save, plus uniqueness check ofname:field across all providers - AWG editor with full
[Interface]/[Peer]/[Mihomo]template covering every key the parser understands - DPI files manager — upload
.binfakes to/zapret-fakebin/, edit text lists in/zapret-lists/, with filter - Rule-set builder — paste a payload-format list, get a
RULE_SETxx_BASE64ENV ready
All runtime artifacts (generated config, provider YAMLs, pre-rendered HTML) live in /dev/shm — zero flash wear.
| Path in container | Purpose | Format |
|---|---|---|
/root/.config/mihomo/awg/ |
WireGuard / AmneziaWG configs → become proxy-providers | *.conf |
/root/.config/mihomo/proxies_mount/ |
Custom proxy-providers in mihomo native YAML | *.yaml / *.yml |
/root/.config/mihomo/rule_set_list/ |
Custom rule-set lists in payload format | *.txt (filename = group name) |
/zapret-fakebin/ |
Binary fake-packets used by nfqws --dpi-desync-fake-* |
*.bin |
/zapret-lists/ |
Text domain / IP lists for nfqws lua scripts | *.txt |
You can also attach multiple VETH interfaces to the container — they show up as direct outbounds in mihomo, route to whichever you want via mangle in RouterOS. Inbound traffic to the container must enter via the first VETH only.
LINK1: "vless://uuid@server:443?type=tcp&security=reality&pbk=...#myvless"
GROUP: "youtube"
YOUTUBE_USE: "LINK1"
YOUTUBE_GEOSITE: "youtube"GROUP: "telegram"
TELEGRAM_USE: "tunnel1"
TELEGRAM_GEOSITE: "telegram"
TELEGRAM_GEOIP: "telegram"
TELEGRAM_AS: "AS62041,AS59930,AS62014,AS211157,AS44907"BYEDPI_CMD: "--tlsrec 41+s --udp-fake 1 --oob 1 --auto=torst,redirect,ssl_err --fake -1"
GROUP: "discord,google"
DISCORD_USE: "BYEDPI"
DISCORD_GEOSITE: "discord"
DISCORD_GEOIP: "discord"
GOOGLE_USE: "BYEDPI"
GOOGLE_GEOSITE: "google"
GOOGLE_GEOIP: "google"SOCKS1: "server=192.168.88.10#port=1080#username=user#password=pass"
GROUP: "lan_socks"
LAN_SOCKS_USE: "SOCKS1"
LAN_SOCKS_SRCIPCIDR: "192.168.88.0/24"| ENV | Default | Description |
|---|---|---|
TPROXY |
true |
On RoS ≥ 7.21 with arm64/amd64, the container uses NFTables. true → TProxy in (TCP+UDP); false → Redirect (TCP) + TUN (UDP). |
DNS_MODE |
fake-ip |
DNS enhanced-mode. |
NAMESERVER_POLICY |
— | Per-domain DNS resolver routing. Format: domain1#dns1,domain2#dns2. Docs. |
SNIFFER |
true |
Domain sniffer for domain-based rules when not resolved by mihomo. |
FAKE_IP_RANGE |
198.18.0.0/15 |
fake-ip pool. |
FAKE_IP_TTL |
1 |
fake-ip cache TTL (seconds). |
FAKE_IP_FILTERxx |
— | Rules list for DNS server in rule mode. |
| ENV | Default | Description |
|---|---|---|
LOG_LEVEL |
error |
mihomo log level: silent/error/warning/info/debug. Docs. |
EXTERNAL_UI_URL |
MetaCube zip | Source zip for the panel served on :9090. Docs. |
UI_SECRET |
— | Secret for the external controller (port 9090). Empty = no auth (LAN-only setups). |
| ENV | Default | Description |
|---|---|---|
HEALTHCHECK_PROVIDER |
true |
true → checks use HEALTHCHECK_*. false → checks use GROUP_URL/XXX_URL/etc. (per-group). |
HEALTHCHECK_URL |
https://www.gstatic.com/generate_204 |
Default URL. |
HEALTHCHECK_URL_STATUS |
204 |
Expected status. |
HEALTHCHECK_INTERVAL |
120 |
Interval (seconds). Docs. |
HEALTHCHECK_URL_BYEDPI |
https://www.facebook.com |
URL for BYEDPI provider. |
HEALTHCHECK_URL_STATUS_BYEDPI |
200 |
Expected status for BYEDPI provider. |
HEALTHCHECK_URL_ZAPRET |
https://www.facebook.com |
URL for all ZAPRET/ZAPRET2 providers. |
HEALTHCHECK_URL_STATUS_ZAPRET |
200 |
Expected status for ZAPRET/ZAPRET2 providers. |
| ENV | Default | Description |
|---|---|---|
BYEDPI_CMDxx |
— | ByeDPI strategy. BYEDPI_CMD → outbound BYEDPI; BYEDPI_CMD1 → BYEDPI_1; etc. Pick strategies with byedpi-orchestrator. |
ZAPRET_CMDxx |
— | Zapret/nfqws strategy. Bundled fakes in /zapret-fakebin/ (e.g. quic_initial_www_google_com.bin) and lists in /zapret-lists/ (ipset-all.txt, list-general.txt, etc.). |
ZAPRET2_CMDxx |
— | Zapret2/nfqws2 strategy. |
ZAPRET2_WG_CMD |
(default with quic_initial_vk_com blob) |
Dedicated nfqws2 strategy for WireGuard handshake routing. |
ZAPRET_PACKETSxx |
12 |
Number of first packets routed through the nfqws queue. xx overrides per-provider. Non-positive values = unlimited (always queued). |
ZAPRET2_PACKETSxx |
12 |
Same for nfqws2. |
| ENV | Default | Description |
|---|---|---|
LINK0, LINK1, … |
— | Single proxy URL: vless://, vmess://, ss://, trojan://, vpn://. Each creates a proxy-provider. |
SUB_LINK0, SUB_LINK1, … |
— | Subscription URL (http(s)://...). One proxy-provider per sub, supports per-sub HWID via headers. |
SUB_LINKxx_PROXY |
DIRECT |
Which proxy is used to fetch the subscription. Example: SUB_LINK1_PROXY=proxies1. |
SUB_LINKxx_HEADERS |
— | Custom HTTP headers for the sub request. Format: key1=val1#key2=val2. HWID example: x-hwid=...#x-device-os=...#x-ver-os=...#x-device-model=...#user-agent=.... |
SUB_LINK_INTERVAL |
3600 |
Default refresh interval (s) for all subs. |
SUB_LINKxx_INTERVAL |
inherits SUB_LINK_INTERVAL |
Override interval per sub. |
SUB_LINKxx_FILTER |
— | Provider-level filter regex — keep only nodes whose name matches. Multiple patterns separated by |. |
SUB_LINKxx_EXCLUDE_FILTER |
— | Provider-level exclude-filter regex. |
SUB_LINKxx_EXCLUDE_TYPE |
— | Provider-level exclude-type — list of Adapter Type (case-insensitive) via |. Example: vmess|direct. |
SUB_LINKxx_ADDITIONAL_PREFIX |
— | Goes into override.additional-prefix — fixed prefix for every node name. |
SUB_LINKxx_ADDITIONAL_SUFFIX |
— | Goes into override.additional-suffix — fixed suffix for every node name. |
SOCKS0, SOCKS1, … |
— | SOCKS5 proxy. Format: server=ip#port=1080#username=#password=#tls=#fingerprint=#skip-cert-verify=#udp=#ip-version=. Docs. |
XXX_DIALER_PROXY |
— | Override dialer-proxy — route this provider's connections through another group. Example: LINK1_DIALER_PROXY=YouTube. |
GROUP declares the set of named groups. For each group XXX (uppercased), prefix-ENV variants below are honored.
💡 In addition to user-defined groups, three "system" groups are hardwired in entrypoint:
GROUP_*(defaults for every group),GLOBAL_*(the special GLOBAL group) andDNS_*(a dedicated group for DNS resolution). All three accept the same prefix ENVs as the table below.
| ENV | Default | Description |
|---|---|---|
GROUP |
— | Comma-separated list of proxy groups. telegram,youtube,google,ai,geoblock → groups TELEGRAM, YOUTUBE, GOOGLE, AI, GEOBLOCK. A group is created only if it has at least one resource (XXX_*) or XXX_USE. |
XXX_TYPE |
select |
Group type: select / url-test / fallback / load-balance / relay. |
XXX_USE |
all providers in order: LINKs, SUB_LINKs, WG/AWG, BYEDPI, DIRECT | Subset of providers to include. Example: YOUTUBE_USE=BYEDPI,LINK1. |
XXX_PROXIES |
— | Explicit proxies (specific nodes, not providers), comma-separated. Alternative/addition to XXX_USE. |
XXX_FILTER |
— | Provider name filter regex. Example: RU|BYEDPI. |
XXX_EXCLUDE |
— | Exclude regex. |
XXX_EXCLUDE_TYPE |
— | Exclude by type. Example: vmess|direct. |
XXX_DNS |
— | DNS resolver for this group's domain rules. Example: https://dns.google/dns-query#disable-qtype-65=true&disable-ipv6=true. |
XXX_ICON |
— | URL for the group's icon. |
XXX_HIDDEN |
false |
Hide the group from mihomo's WebUI. |
GROUP_URL / XXX_URL |
https://www.gstatic.com/generate_204 |
Per-group health-check URL when HEALTHCHECK_PROVIDER=false and XXX_TYPE is url-test/fallback/load-balance. |
GROUP_URL_STATUS / XXX_URL_STATUS |
204 |
Expected status for the above. |
GROUP_INTERVAL / XXX_INTERVAL |
60 |
Check interval (seconds). |
GROUP_TOLERANCE / XXX_TOLERANCE |
20 |
URL-test tolerance in ms. |
GROUP_STRATEGY / XXX_STRATEGY |
consistent-hashing |
Load-balance strategy. |
Each entry creates an automatic rule targeting the group XXX. XXX_GEOSITE/GEOIP/AS also build a rule-set in .mrs format from the meta-rules-dat repo.
| ENV | Description |
|---|---|
XXX_GEOSITE |
Comma-separated geosite names. Example: GEOBLOCK_GEOSITE=intel,openai,xai. |
XXX_GEOIP |
Comma-separated geoip. Example: GEOBLOCK_GEOIP=netflix. |
XXX_AS |
AS numbers. Example: TELEGRAM_AS=AS62041,AS59930,AS62014,AS211157,AS44907. |
XXX_DOMAIN |
Exact DOMAIN matches. |
XXX_SUFFIX |
DOMAIN-SUFFIX matches. |
XXX_KEYWORD |
DOMAIN-KEYWORD matches. |
XXX_IPCIDR |
IP-CIDR ranges. |
XXX_SRCIPCIDR |
SRC-IP-CIDR — route by source. Example: SOCKS_SRCIPCIDR=192.168.88.37/32,192.168.88.65/32. |
XXX_DSCP |
Marks this group's traffic with a DSCP value (0–63). Example: YOUTUBE_DSCP=10. |
XXX_PRIORITY |
Position of this group's rules in the rules list. Lower = earlier. Shared priority space with RULESxx. Default ≥1000. |
| ENV | Description |
|---|---|
RULE_SETxx_BASE64 |
<base64>#<name> — <base64> is base64-encoded payload list. Creates a rule-set + group <name> with priority ≥2000. The WebUI's Rule-sets → New base64 builds these for you. |
RULESxx |
Raw mihomo rule where xx is the priority. Example: RULES1=AND,((NETWORK,udp),(DST-PORT,443)),REJECT drops QUIC first. |
First, enable container support (if not enabled already):
/system/device-mode/print
/system/device-mode/update mode=advanced container=yes traffic-gen=yes
You have ~5 minutes to confirm via power-cycle or briefly pressing any physical button on the device.
Then paste the snippet below into RouterOS terminal:
:global currentVersion [/system resource get version];
:global currentMinor [:pick $currentVersion ([:find $currentVersion "."] + 1) ([:find $currentVersion "."] + 3)];
:global r
:global statusPackage false
:global statusDeviceMode [/system/device-mode/get container]
:if ([:len [/system/package/find name=container available=no disabled=no]] >0) do={
:set statusPackage true
} else={
:put "Please check the installation of the container package"
}
:if ($statusDeviceMode=false) do={
:put "Please check /system/device-mode/print container enable"
}
:if ($currentMinor >= 21) do={
:put "Current version RouterOS 7.$currentMinor"
:set r [/tool fetch url=https://raw.githubusercontent.com/Medium1992/mihomo-proxy-ros/refs/heads/main/script21.rsc mode=https output=user as-value]
}
:if ($currentMinor = 20) do={
:put "Current version RouterOS 7.$currentMinor"
:set r [/tool fetch url=https://raw.githubusercontent.com/Medium1992/mihomo-proxy-ros/refs/heads/main/script.rsc mode=https output=user as-value]
}
:if ($currentMinor < 20) do={
:put "Current version RouterOS $currentVersion"
:put "Update to at least version RouterOS 7.20"
}
:if (($r->"status")="finished" and $statusPackage=true and $statusDeviceMode=true) do={
:global content ($r->"data")
:if ([:len $content] > 0) do={
:global s [:parse $content]
:log warning "script loading completed and started"
:put "script loading completed and started"
$s
/system/script/environment/remove [find where ]
}
}
During execution the script asks for:
- one proxy URL (
vless://,vmess://,ss://,trojan://) - optional subscription URL (
http(s)://...)
…then sets up router config, mangle + routing, container install, and the initial domain pool.
After install, fine-tune everything either via the WebUI on :80 or via the existing helper repos:
See docker-compose.yml for a standalone example.
PRs welcome — read the Code of Conduct and CONTRIBUTING.md first.
Report sensitive issues per SECURITY.md.
If this saved you time configuring MikroTik and its scripts:
- USDT (TRC20):
TWDDYD1nk5JnG6FxvEu2fyFqMCY9PcdEsJ - boosty.to/petersolomon/donate




