From fa0650ed854fe0f6f5aca52bf3751e072187962f Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Fri, 13 Nov 2026 07:53:26 +0300 Subject: [PATCH] Make IPv6 permissions explicit and add loopback-compatible blocking mode --- assets/strings.json | 4 ++ docs/configuration.md | 19 ++++++++- docs/development.md | 9 +++++ docs/en/profiles-and-routing.md | 18 ++++++++- docs/ru/profiles-and-routing.md | 18 ++++++++- scripts/core_smoke.py | 40 ++++++++++++++++++- src/WindowsMegaProxy.Core/Configuration.cs | 6 ++- .../ConfigurationFields.cs | 7 ++-- src/WindowsMegaProxy.Core/ImportReview.cs | 2 + .../PortableConfiguration.cs | 5 ++- src/WindowsMegaProxy.Core/Profiles.cs | 2 +- src/WindowsMegaProxy.Core/SettingsWriter.cs | 2 +- src/WindowsMegaProxy.Core/TunAddresses.cs | 31 ++++++++++---- src/WindowsMegaProxy.Runtime/Engine.cs | 10 ++--- src/WindowsMegaProxy/MainWindow.xaml | 13 ++++++ src/WindowsMegaProxy/MainWindow.xaml.cs | 8 +++- src/WindowsMegaProxy/ProfileWindow.xaml | 4 ++ src/WindowsMegaProxy/ProfileWindow.xaml.cs | 3 ++ .../ConfigurationContractChecks.cs | 38 +++++++++++++++++- .../ConnectionOptionsChecks.cs | 6 +-- tests/WindowsMegaProxy.Checks/Program.cs | 38 ++++++++++++++---- tests/WindowsMegaProxy.Checks/SniffChecks.cs | 2 +- tests/WindowsMegaProxy.UiChecks/Ipv6Checks.cs | 8 ++-- .../WindowsMegaProxy.UiChecks/Ipv6UiChecks.cs | 6 +-- tests/WindowsMegaProxy.UiChecks/Program.cs | 11 +++++ 25 files changed, 263 insertions(+), 47 deletions(-) diff --git a/assets/strings.json b/assets/strings.json index d28ac57..9297230 100644 --- a/assets/strings.json +++ b/assets/strings.json @@ -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 требует поддержки сервера.", diff --git a/docs/configuration.md b/docs/configuration.md index 6d2c6a5..85f819f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 @@ -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. diff --git a/docs/development.md b/docs/development.md index 44cd821..29969a3 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. diff --git a/docs/en/profiles-and-routing.md b/docs/en/profiles-and-routing.md index 93185f2..a304bd9 100644 --- a/docs/en/profiles-and-routing.md +++ b/docs/en/profiles-and-routing.md @@ -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. @@ -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. diff --git a/docs/ru/profiles-and-routing.md b/docs/ru/profiles-and-routing.md index 3838c6d..4d4b5c8 100644 --- a/docs/ru/profiles-and-routing.md +++ b/docs/ru/profiles-and-routing.md @@ -29,7 +29,7 @@ HTTP/1.1 использует комплектное стабильное ядр IP-адреса, подсети и списки Podkop. Действия: активный профиль, системный маршрут или блокировка. Галочка отключает правило без удаления. -Windows всегда разрешает IPv6 и игнорирует импортированный `routing.allowIpv6`. +IPv6 включается вручную: в профиле (`routing.allowIpv6`) и для системного маршрута в настройках подключения (глобальное `routing.systemAllowIpv6`). Обе опции по умолчанию выключены. Для маршрута только через профиль нужна опция профиля, только через систему — системная опция, для смешанного режима — обе. Отключённые правила игнорируются, безусловное правило «всё» заменяет маршрут по умолчанию. Блокировки, служебный обход ядра и обход локальных сетей не считаются отдельным маршрутом в Интернет. Автоопределения нет; изменения применяются после переподключения. Явные правила маршрутизации и блокировки продолжают действовать. SSH и HTTPS только через TCP не предоставляют пересылку UDP. @@ -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 разрешён настройками маршрутов, эта опция не влияет на работу. +Изменение применяется после переподключения. diff --git a/scripts/core_smoke.py b/scripts/core_smoke.py index 97c9831..f20ef01 100644 --- a/scripts/core_smoke.py +++ b/scripts/core_smoke.py @@ -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: @@ -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 @@ -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): @@ -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 diff --git a/src/WindowsMegaProxy.Core/Configuration.cs b/src/WindowsMegaProxy.Core/Configuration.cs index 18619e0..c1f81ee 100644 --- a/src/WindowsMegaProxy.Core/Configuration.cs +++ b/src/WindowsMegaProxy.Core/Configuration.cs @@ -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; } @@ -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 @@ -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") diff --git a/src/WindowsMegaProxy.Core/ConfigurationFields.cs b/src/WindowsMegaProxy.Core/ConfigurationFields.cs index e96efbd..867b839 100644 --- a/src/WindowsMegaProxy.Core/ConfigurationFields.cs +++ b/src/WindowsMegaProxy.Core/ConfigurationFields.cs @@ -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) @@ -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"); } } @@ -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"); diff --git a/src/WindowsMegaProxy.Core/ImportReview.cs b/src/WindowsMegaProxy.Core/ImportReview.cs index 3224df1..c85264c 100644 --- a/src/WindowsMegaProxy.Core/ImportReview.cs +++ b/src/WindowsMegaProxy.Core/ImportReview.cs @@ -127,6 +127,8 @@ public Settings Apply(ISet selected, bool applyGlobals, ISet 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(); diff --git a/src/WindowsMegaProxy.Core/PortableConfiguration.cs b/src/WindowsMegaProxy.Core/PortableConfiguration.cs index 829d9c6..cdd3557 100644 --- a/src/WindowsMegaProxy.Core/PortableConfiguration.cs +++ b/src/WindowsMegaProxy.Core/PortableConfiguration.cs @@ -99,6 +99,7 @@ public static Settings Import(string text, Settings current) if (windows.ContainsKey("theme")) candidate.Theme = String(windows, "theme"); if (windows.ContainsKey("alwaysMinimizeToTray")) candidate.AlwaysMinimizeToTray = windows["alwaysMinimizeToTray"]!.GetValue(); if (windows.ContainsKey("tlsFingerprint")) candidate.TlsFingerprint = String(windows, "tlsFingerprint"); + if (windows.ContainsKey("blockDisabledIpv6")) candidate.BlockDisabledIpv6 = windows["blockDisabledIpv6"]!.GetValue(); if (windows.ContainsKey("coreLogLevel")) candidate.CoreLogLevel = String(windows, "coreLogLevel"); if (windows.ContainsKey("bootstrapDnsProvider")) candidate.BootstrapDnsProvider = String(windows, "bootstrapDnsProvider"); if (windows.ContainsKey("launchAtSignIn")) candidate.LaunchAtSignIn = windows["launchAtSignIn"]!.GetValue(); @@ -111,6 +112,7 @@ public static Settings Import(string text, Settings current) if (Configuration.TlsFingerprints.Contains(value)) candidate.TlsFingerprint = value; } if (root["diagnosticLogLimitMb"] is { } logSize) candidate.LogSizeMb = Math.Clamp(logSize.GetValue(), 1, 100); + if (root["routing"]?["systemAllowIpv6"] is { } systemIpv6) candidate.SystemAllowIpv6 = systemIpv6.GetValue(); if (root["routing"]?["bypassLocalNetworks"] is { } bypass) candidate.BypassLan = bypass.GetValue(); var requestedId = root["activeProfileId"]?.GetValue(); if (requestedId is not null && !candidate.Profiles.Any(profile => profile.Id == requestedId)) throw Invalid(Ui.T("The active profile is missing from the configuration.")); @@ -159,6 +161,7 @@ public static JsonObject Export(Settings settings, bool includePasswords = false windows["theme"] = settings.Theme; windows["alwaysMinimizeToTray"] = settings.AlwaysMinimizeToTray; windows["bootstrapDnsProvider"] = settings.BootstrapDnsProvider; + windows["blockDisabledIpv6"] = settings.BlockDisabledIpv6; windows["coreLogLevel"] = settings.CoreLogLevel; windows["launchAtSignIn"] = settings.LaunchAtSignIn ?? false; windows["rules"] = JsonSerializer.SerializeToNode(settings.Rules, PortableOptions); @@ -170,7 +173,7 @@ public static JsonObject Export(Settings settings, bool includePasswords = false windows.Remove("bypassLocalNetworks"); result["tls"] = new JsonObject { ["fingerprint"] = ConfigurationFields.NormalizeFingerprint(settings.TlsFingerprint) }; result["diagnosticLogLimitMb"] = settings.LogSizeMb; - result["routing"] = new JsonObject { ["bypassLocalNetworks"] = settings.BypassLan }; + result["routing"] = new JsonObject { ["bypassLocalNetworks"] = settings.BypassLan, ["systemAllowIpv6"] = settings.SystemAllowIpv6 }; result["windows"] = windows; result = ConfigurationFields.Project(result); RemoveSecrets(result, includePasswords, includePrivateKeys); diff --git a/src/WindowsMegaProxy.Core/Profiles.cs b/src/WindowsMegaProxy.Core/Profiles.cs index 3b61408..214b6a6 100644 --- a/src/WindowsMegaProxy.Core/Profiles.cs +++ b/src/WindowsMegaProxy.Core/Profiles.cs @@ -34,7 +34,7 @@ public ProxyProfile() { } [JsonIgnore] public bool PreferHttp3 => Type is "HTTPS" or "HTTPS_JUMP" && (Proxy["preferHttp3"]?.GetValue() ?? false); [JsonIgnore] public bool PreferHttp2 => Type is "HTTPS" or "HTTPS_JUMP" && ((Proxy["preferHttp2"]?.GetValue() ?? false) || PreferHttp3); [JsonIgnore] public bool UsesHttp3 => Type == "MASQUE" || PreferHttp3; - [JsonIgnore] public bool AllowIpv6 => true; + [JsonIgnore] public bool AllowIpv6 => Data["routing"]?["allowIpv6"]?.GetValue() ?? false; [JsonIgnore] public string Endpoint => $"{Host}:{Port}"; [JsonIgnore] public string Description => $"{Endpoint} · {TypeLabel}"; [JsonIgnore] public string SelectionLabel => $"{Name} · {TypeLabel}"; diff --git a/src/WindowsMegaProxy.Core/SettingsWriter.cs b/src/WindowsMegaProxy.Core/SettingsWriter.cs index 2bd3e5f..fbc955c 100644 --- a/src/WindowsMegaProxy.Core/SettingsWriter.cs +++ b/src/WindowsMegaProxy.Core/SettingsWriter.cs @@ -9,7 +9,7 @@ namespace WindowsMegaProxy; public sealed record SettingsAnalysis(Settings Settings, string? Signature, string? Error) { public static bool SameNetwork(Settings a, Settings b) => a.DefaultAction == b.DefaultAction && a.BypassLan == b.BypassLan && - a.CoreLogLevel == b.CoreLogLevel && a.TunMtu == b.TunMtu && a.TlsFingerprint == b.TlsFingerprint && a.BootstrapDnsProvider == b.BootstrapDnsProvider && a.ActiveProfileId == b.ActiveProfileId && + a.BlockDisabledIpv6 == b.BlockDisabledIpv6 && a.SystemAllowIpv6 == b.SystemAllowIpv6 && a.CoreLogLevel == b.CoreLogLevel && a.TunMtu == b.TunMtu && a.TlsFingerprint == b.TlsFingerprint && a.BootstrapDnsProvider == b.BootstrapDnsProvider && a.ActiveProfileId == b.ActiveProfileId && AppUpdatePolicy.ThroughProxy(a) == AppUpdatePolicy.ThroughProxy(b) && SshConfiguration.AuthMode(a) == SshConfiguration.AuthMode(b) && JsonNode.DeepEquals(a.SharedOptions["ssh"]?["fingerprint"], b.SharedOptions["ssh"]?["fingerprint"]) && diff --git a/src/WindowsMegaProxy.Core/TunAddresses.cs b/src/WindowsMegaProxy.Core/TunAddresses.cs index b3133e4..cd631d3 100644 --- a/src/WindowsMegaProxy.Core/TunAddresses.cs +++ b/src/WindowsMegaProxy.Core/TunAddresses.cs @@ -10,16 +10,31 @@ namespace WindowsMegaProxy; public static class TunAddresses { - public static bool HasIpv6Uplink(IEnumerable addresses, IEnumerable gateways) => - addresses.Any(address => address.AddressFamily == AddressFamily.InterNetworkV6 && - !address.IsIPv6LinkLocal && !address.IsIPv6SiteLocal && !IPAddress.IsLoopback(address) && - !address.Equals(IPAddress.IPv6Any) && (address.GetAddressBytes()[0] & 0xfe) != 0xfc) && - gateways.Any(address => address.AddressFamily == AddressFamily.InterNetworkV6 && - !address.Equals(IPAddress.IPv6Any)); + public static bool Enabled(Settings settings) + { + bool proxy = false, system = false, terminal = false; + void Include(string action) + { + proxy |= action == "proxy"; + system |= action is "system" or "direct"; + } + foreach (var rule in settings.Rules.Where(rule => rule.Enabled)) + { + Include(rule.Action); + if (rule.Source == "all" && string.IsNullOrWhiteSpace(rule.Destination)) + { + terminal = true; + break; + } + } + if (!terminal) Include(settings.DefaultAction); + return (proxy || system) && (!proxy || settings.ActiveProfile?.AllowIpv6 == true) && + (!system || settings.SystemAllowIpv6); + } - public static void Apply(JsonObject config, (string Ipv4, string Ipv6) addresses, bool ipv6) + public static void Apply(JsonObject config, (string Ipv4, string Ipv6) addresses, bool ipv6, bool blockDisabledIpv6 = true) { - config["inbounds"]![0]!["address"] = ipv6 ? new JsonArray(addresses.Ipv4, addresses.Ipv6) : new JsonArray(addresses.Ipv4); + config["inbounds"]![0]!["address"] = ipv6 || !blockDisabledIpv6 ? new JsonArray(addresses.Ipv4, addresses.Ipv6) : new JsonArray(addresses.Ipv4); if (ipv6) return; config["dns"]!["strategy"] = "ipv4_only"; config["route"]!["rules"]!.AsArray().Insert(0, new JsonObject diff --git a/src/WindowsMegaProxy.Runtime/Engine.cs b/src/WindowsMegaProxy.Runtime/Engine.cs index 66611b8..a28cc4a 100644 --- a/src/WindowsMegaProxy.Runtime/Engine.cs +++ b/src/WindowsMegaProxy.Runtime/Engine.cs @@ -151,16 +151,14 @@ private async Task StartCoreAsync(Settings settings, bool testOnly, Cancellation var adapters = NetworkInterface.GetAllNetworkInterfaces() .Where(adapter => !string.Equals(adapter.Name, AppIdentity.DisplayName, StringComparison.OrdinalIgnoreCase)) .ToArray(); - bool ipv6 = adapters.Any(adapter => adapter.OperationalStatus == OperationalStatus.Up && - adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && - TunAddresses.HasIpv6Uplink(adapter.GetIPProperties().UnicastAddresses.Select(item => item.Address), - adapter.GetIPProperties().GatewayAddresses.Select(item => item.Address))); + bool ipv6 = TunAddresses.Enabled(settings); var networks = adapters.SelectMany(adapter => adapter.GetIPProperties().UnicastAddresses) .Select(address => TunAddresses.Network(address.Address, address.PrefixLength)); return (Addresses: TunAddresses.Select(networks, AppIdentity.IsDev), Ipv6: ipv6); }, cancellationToken).ConfigureAwait(false); - TunAddresses.Apply(config, network.Addresses, network.Ipv6); - Log?.Invoke($"TUN interface {AppIdentity.DisplayName}: IPv4={network.Addresses.Ipv4}; IPv6={(network.Ipv6 ? network.Addresses.Ipv6 : "disabled (no IPv6 uplink)")}."); + config["inbounds"]![0]!["address"] = network.Ipv6 || !settings.BlockDisabledIpv6 + ? new JsonArray(network.Addresses.Ipv4, network.Addresses.Ipv6) : new JsonArray(network.Addresses.Ipv4); + Log?.Invoke($"TUN interface {AppIdentity.DisplayName}: IPv4={network.Addresses.Ipv4}; IPv6={(network.Ipv6 ? network.Addresses.Ipv6 : settings.BlockDisabledIpv6 ? "disabled by configuration" : "disabled for routed traffic; TUN address retained")}."); } var cache = new PodkopCache(Path.Combine(stateDirectory, "podkop"), message => Log?.Invoke(message)); foreach (string id in PodkopCatalog.Used(settings)) await cache.GetAsync(id, cancellationToken, diff --git a/src/WindowsMegaProxy/MainWindow.xaml b/src/WindowsMegaProxy/MainWindow.xaml index 9215b63..d09ae6b 100644 --- a/src/WindowsMegaProxy/MainWindow.xaml +++ b/src/WindowsMegaProxy/MainWindow.xaml @@ -1275,6 +1275,19 @@ + + +