Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/strings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"Block IPv6 when disabled in MegaProxy": "Блокировать IPv6 при отключении в MegaProxy",
"May block local IPv6 connections (::1). Turn off to retain loopback; IPv6 traffic through MegaProxy remains blocked. Applies after reconnection.": "Может блокировать локальные IPv6-соединения (::1). Отключите для сохранения loopback; IPv6-трафик через MegaProxy останется заблокирован. Применяется после переподключения.",
"Enable IPv6 destinations": "Разрешить подключения к IPv6-адресам",
"Enable IPv6 for system routing": "Разрешить IPv6 для системного маршрута",
"sing-box log level": "Уровень логов sing-box",
"Applies after reconnection. Debug and trace include destination addresses and application paths and increase log volume.": "Применяется после переподключения. Debug и trace включают адреса назначения и пути приложений и увеличивают объём журнала.",
"SOCKS5 does not encrypt the server connection; UDP requires server support.": "SOCKS5 не шифрует соединение с сервером; UDP требует поддержки сервера.",
Expand Down
19 changes: 17 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ when imported key pins or `acceptAnyHostKey=false` are present.
When a global SSH fingerprint is present, the profile editor displays and edits
that global selection and explains that it affects all SSH profiles.

Windows always allows IPv6. Imported `routing.allowIpv6` does not restrict it and
is discarded. Explicit destination block rules still work. Android package routing
IPv6 is opt-in. Profile `routing.allowIpv6` controls proxy destinations; root `routing.systemAllowIpv6` controls the system route. Both default to false. Proxy-only routing requires the profile option, system-only routing the system option, and mixed routing both. Enabled rules are evaluated in order; an unconditional all-applications/all-destinations rule overrides the fallback. Block rules, internal core bypass and local-network bypass do not count as a separate Internet route. No automatic uplink detection is performed. Reconnect to apply changes. Android package routing
and other platforms' routing sections do not become Windows rules.

DNS supports the named providers and custom HTTPS/TCP/UDP endpoints; see
Expand Down Expand Up @@ -117,3 +116,19 @@ interval and pause state. Failed/stale downloads never replace a working snapsho
Refreshing configuration does not restart an established tunnel. Reconnect applies
network changes. Ownership, status, timestamps, caches, downloaded updates and
permissions stay local and are not exported. Manual refresh works while paused.

### Blocking disabled IPv6

Connection settings include **Block IPv6 when disabled in MegaProxy**, enabled
by default (`windows.blockDisabledIpv6`, default `true`, included in config
import/export). An omitted field preserves the current value on import.
When IPv6 is disabled by the routing permissions, this removes the TUN IPv6
address. With strict routing enabled, sing-tun then blocks IPv6 system-wide,
including local connections to `::1` (Windows error 10013).

Turn this option off to retain the TUN IPv6 address and avoid that global block.
MegaProxy still uses IPv4-only DNS results and rejects IPv6 traffic entering its
core. Strict routing and DNS protection stay enabled; local DNS on `::1:53`
may still be blocked by the separate DNS filter. Traffic that bypasses TUN is
not covered by the core rejection rule. The option has no effect when IPv6 is
permitted by the routing settings. Reconnect to apply changes.
9 changes: 9 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ IPv6 connectivity or Wintun. Logs and test reports remain under `artifacts`.
without automatic routes; it requires administrator rights. CI runs this check.
For release signing, packaging, CI gates and retries, see
[Installer and release automation](../installer/README.md).

The CI native-core `core_smoke.py --tun` checks also exercise strict routing with
IPv6 blocked, disabled with loopback compatibility, and enabled, using both DLLs.
They verify IPv4/IPv6 TCP loopback and that the IPv6 block disappears after core
shutdown. Routes are restricted to documentation subnets, but WFP and DNS filters
are system-wide while the test runs: use a disposable Windows runner with admin
rights, not a workstation with an active VPN. A failed baseline loopback check is
an error, not a skip. The configuration suite covers all 24 combinations of route
mode, profile/system permissions and the blocking option, including DNS policy.
18 changes: 17 additions & 1 deletion docs/en/profiles-and-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ destination domains, wildcard domains, IP addresses/subnets or Podkop lists.
Rules can route through the active profile, use the system route or block traffic.
Disable a rule with its checkbox instead of deleting it.

Windows always allows IPv6 and ignores imported `routing.allowIpv6`.
IPv6 is opt-in. Profile `routing.allowIpv6` controls proxy destinations; root `routing.systemAllowIpv6` controls the system route. Both default to false. Proxy-only routing requires the profile option, system-only routing the system option, and mixed routing both. Enabled rules are evaluated in order; an unconditional all-applications/all-destinations rule overrides the fallback. Block rules, internal core bypass and local-network bypass do not count as a separate Internet route. No automatic uplink detection is performed. Reconnect to apply changes.
Explicit system-route/block rules and local-network bypass still apply.
SSH and TCP-only HTTPS do not provide UDP forwarding.

Expand Down Expand Up @@ -97,3 +97,19 @@ ambiguous destination-domain rules are skipped. Existing transfers can be interr

For missing tray icons, service connection problems or failed updates, see
[Troubleshooting](troubleshooting.md).

### Blocking disabled IPv6

Connection settings include **Block IPv6 when disabled in MegaProxy**, enabled
by default (`windows.blockDisabledIpv6`, default `true`, included in config
import/export). An omitted field preserves the current value on import.
When IPv6 is disabled by the routing permissions, this removes the TUN IPv6
address. With strict routing enabled, sing-tun then blocks IPv6 system-wide,
including local connections to `::1` (Windows error 10013).

Turn this option off to retain the TUN IPv6 address and avoid that global block.
MegaProxy still uses IPv4-only DNS results and rejects IPv6 traffic entering its
core. Strict routing and DNS protection stay enabled; local DNS on `::1:53`
may still be blocked by the separate DNS filter. Traffic that bypasses TUN is
not covered by the core rejection rule. The option has no effect when IPv6 is
permitted by the routing settings. Reconnect to apply changes.
18 changes: 17 additions & 1 deletion docs/ru/profiles-and-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ HTTP/1.1 использует комплектное стабильное ядр
IP-адреса, подсети и списки Podkop. Действия: активный профиль, системный маршрут
или блокировка. Галочка отключает правило без удаления.

Windows всегда разрешает IPv6 и игнорирует импортированный `routing.allowIpv6`.
IPv6 включается вручную: в профиле (`routing.allowIpv6`) и для системного маршрута в настройках подключения (глобальное `routing.systemAllowIpv6`). Обе опции по умолчанию выключены. Для маршрута только через профиль нужна опция профиля, только через систему — системная опция, для смешанного режима — обе. Отключённые правила игнорируются, безусловное правило «всё» заменяет маршрут по умолчанию. Блокировки, служебный обход ядра и обход локальных сетей не считаются отдельным маршрутом в Интернет. Автоопределения нет; изменения применяются после переподключения.
Явные правила маршрутизации и блокировки продолжают действовать.
SSH и HTTPS только через TCP не предоставляют пересылку UDP.

Expand Down Expand Up @@ -96,3 +96,19 @@ HTTPS-подписки поддерживают резервные адреса

Если пропал значок, нет связи со службой или не завершилось обновление, см.
[Решение проблем](troubleshooting.md).

### Блокировка отключённого IPv6

В настройках подключения есть опция **«Блокировать IPv6 при отключении в
MegaProxy»**, включённая по умолчанию (`windows.blockDisabledIpv6`, `true`).
Она импортируется и экспортируется; отсутствие поля сохраняет текущее значение.
Если IPv6 запрещён настройками маршрутов, опция убирает IPv6-адрес TUN.
При строгой маршрутизации sing-tun блокирует IPv6 системным фильтром, в том числе
локальные соединения с `::1` (ошибка Windows 10013).

Если снять опцию, IPv6-адрес TUN сохраняется и глобальный запрет не создаётся.
При этом DNS остаётся IPv4-only, а IPv6-трафик, поступающий в ядро, отклоняется.
Строгая маршрутизация и защита DNS сохраняются: локальный DNS на `::1:53` может
блокироваться отдельным DNS-фильтром. Трафик в обход TUN не покрывается запретом
в ядре. Если IPv6 разрешён настройками маршрутов, эта опция не влияет на работу.
Изменение применяется после переподключения.
40 changes: 39 additions & 1 deletion scripts/core_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,40 @@ def server():
assert not errors, errors


def tun(host, variant):
def loopback(family, blocked=False):
address = "::1" if family == socket.AF_INET6 else "127.0.0.1"
with socket.socket(family) as listener, socket.socket(family) as client:
listener.bind((address, 0))
listener.listen()
listener.settimeout(3)
client.settimeout(3)
try:
client.connect(listener.getsockname())
except OSError as error:
if not blocked or error.winerror != 10013:
raise
return
assert not blocked, "Strict IPv4-only TUN did not block IPv6 loopback."
with listener.accept()[0] as peer:
peer.settimeout(3)
client.sendall(b"loopback")
assert receive(peer, 8) == b"loopback"


def tun(host, variant, ipv6_mode=None):
if not ct.windll.shell32.IsUserAnAdmin():
raise RuntimeError("TUN smoke checks require an administrator token.")
config = CONFIG | {"inbounds": [{"type": "tun", "interface_name": "MegaProxy DLL smoke",
"address": ["192.0.2.1/30"], "auto_route": False, "stack": "system"}]}
if ipv6_mode is not None:
loopback(socket.AF_INET6)
inbound = config["inbounds"][0]
inbound.update(auto_route=True, strict_route=True, dns_mode="hijack",
route_address=["192.0.2.0/30", "2001:db8:487::/126"])
if ipv6_mode != "blocked":
inbound["address"].append("2001:db8:487::1/126")
if ipv6_mode != "enabled":
config["route"] = {"rules": [{"ip_version": 6, "action": "reject"}], "final": "direct"}
ready = threading.Event()
messages = []
with sp.Popen([str(host), variant], stdin=sp.PIPE, stdout=sp.DEVNULL, stderr=sp.PIPE) as process:
Expand All @@ -118,6 +147,9 @@ def drain():
process.stdin.flush()
if not ready.wait(20):
raise RuntimeError("TUN did not start: " + "".join(messages))
if ipv6_mode is not None:
loopback(socket.AF_INET)
loopback(socket.AF_INET6, blocked=ipv6_mode == "blocked")
process.stdin.close()
process.wait(timeout=10)
assert process.returncode == 0, messages
Expand All @@ -126,6 +158,10 @@ def drain():
process.kill()
process.wait()
reader.join(timeout=5)
if ipv6_mode is not None:
loopback(socket.AF_INET6)
if ipv6_mode is not None:
print(f"{variant} TUN IPv6 {ipv6_mode}: loopback and filter cleanup passed.", flush=True)


def main(signed=False, check_tun=False):
Expand All @@ -145,6 +181,8 @@ def main(signed=False, check_tun=False):
traffic(dev_host, variant)
if check_tun:
tun(dev_host, variant)
for ipv6_mode in ("blocked", "compatible", "enabled"):
tun(dev_host, variant, ipv6_mode)
print("DEV lifecycle, invalid configuration and local SOCKS traffic passed for both DLLs.", flush=True)
if not signed:
return
Expand Down
6 changes: 5 additions & 1 deletion src/WindowsMegaProxy.Core/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public sealed class Settings
public JsonObject SharedOptions { get; set; } = new();
public JsonObject WindowsOptions { get; set; } = new();
public string DefaultAction { get; set; } = "system";
public bool BlockDisabledIpv6 { get; set; } = true;
public bool SystemAllowIpv6 { get; set; }
public bool BypassLan { get; set; } = true;
public bool AutoConnect { get; set; }
public bool ResetExistingTcpConnections { get; set; }
Expand Down Expand Up @@ -190,7 +192,7 @@ public static JsonObject Build(Settings settings, int apiPort, string apiSecret,
rules.Add(new JsonObject { ["ip_is_private"] = true, ["action"] = "route", ["outbound"] = "direct" });
if (settings.DefaultAction == "block") rules.Add(new JsonObject { ["action"] = "reject" });

return new JsonObject
var config = new JsonObject
{
["log"] = new JsonObject { ["level"] = settings.CoreLogLevel, ["timestamp"] = true },
["dns"] = new JsonObject
Expand Down Expand Up @@ -218,6 +220,8 @@ public static JsonObject Build(Settings settings, int apiPort, string apiSecret,
},
["experimental"] = new JsonObject { ["clash_api"] = new JsonObject { ["external_controller"] = $"127.0.0.1:{apiPort}", ["secret"] = apiSecret } }
};
TunAddresses.Apply(config, tunAddresses, TunAddresses.Enabled(settings), settings.BlockDisabledIpv6);
return config;
}

public static JsonObject BuildOutbound(ProxyProfile profile, string tag, string tlsFingerprint = "default", bool allowUnresolvedSsh = false, string sshAuthMode = "AUTO")
Expand Down
7 changes: 4 additions & 3 deletions src/WindowsMegaProxy.Core/ConfigurationFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void Proxy(JsonObject proxy, bool jump = false)
Keep(ssh, "fingerprint authMode");
Fingerprint(ssh, "fingerprint", true);
}
if (root["routing"] is JsonObject routing) Keep(routing, "bypassLocalNetworks");
if (root["routing"] is JsonObject routing) Keep(routing, "bypassLocalNetworks systemAllowIpv6");
if (root["failover"] is JsonObject failover) Keep(failover, "mode profileIds");
if (root["subscription"] is JsonObject subscription) Keep(subscription, "url username password intervalMinutes enabled fallbackUrls");
if (root["windows"] is JsonObject windows)
Expand All @@ -72,7 +72,7 @@ void Proxy(JsonObject proxy, bool jump = false)
}
else
{
Keep(windows, "version defaultAction bypassLocalNetworks autoConnect resetExistingTcpConnections logSizeMb reverseDnsCacheHours resolveConnectionHosts showAllConnections language theme alwaysMinimizeToTray tlsFingerprint coreLogLevel bootstrapDnsProvider launchAtSignIn rules tunMtu trafficUnits automaticAppUpdates appUpdatesThroughProxy");
Keep(windows, "version defaultAction bypassLocalNetworks autoConnect resetExistingTcpConnections logSizeMb reverseDnsCacheHours resolveConnectionHosts showAllConnections language theme alwaysMinimizeToTray tlsFingerprint blockDisabledIpv6 coreLogLevel bootstrapDnsProvider launchAtSignIn rules tunMtu trafficUnits automaticAppUpdates appUpdatesThroughProxy");
Fingerprint(windows, "tlsFingerprint");
}
}
Expand All @@ -86,7 +86,8 @@ void Proxy(JsonObject proxy, bool jump = false)
if (!proxy.ContainsKey("knockHost") && profile["browser"] is JsonObject browser && browser["knockHost"] is { } knock) proxy["knockHost"] = knock.DeepClone();
Proxy(proxy);
}
Keep(profile, "id name color countryCode proxy tls dns windows");
Keep(profile, "id name color countryCode proxy tls dns routing windows");
if (profile["routing"] is JsonObject profileRouting) Keep(profileRouting, "allowIpv6");
Tls(profile);
if (profile["dns"] is JsonObject dns) Keep(dns, "provider customDohUrl");
if (profile["windows"] is JsonObject options) Keep(options, "sshHostKey sshJumpHostKey colorMode");
Expand Down
2 changes: 2 additions & 0 deletions src/WindowsMegaProxy.Core/ImportReview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public Settings Apply(ISet<string> selected, bool applyGlobals, ISet<string> rem
result.AlwaysMinimizeToTray = current.AlwaysMinimizeToTray;
result.TlsFingerprint = current.TlsFingerprint; result.LaunchAtSignIn = current.LaunchAtSignIn;
result.BootstrapDnsProvider = current.BootstrapDnsProvider;
result.BlockDisabledIpv6 = current.BlockDisabledIpv6;
result.SystemAllowIpv6 = current.SystemAllowIpv6;
result.CoreLogLevel = current.CoreLogLevel;
result.ResetExistingTcpConnections = current.ResetExistingTcpConnections;
result.WindowsOptions = (JsonObject)current.WindowsOptions.DeepClone();
Expand Down
Loading
Loading