From 8c37371ca36700827993632874983c3d2170fc65 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Thu, 12 Nov 2026 21:57:49 +0300 Subject: [PATCH] Fix profile DNS routing and IPv6 uplink handling; expose core log level --- assets/strings.json | 4 ++- docs/configuration.md | 1 + docs/en/profiles-and-routing.md | 17 +++++++-- docs/ru/profiles-and-routing.md | 17 +++++++-- src/WindowsMegaProxy.Core/Configuration.cs | 25 ++++++++----- .../ConfigurationFields.cs | 2 +- src/WindowsMegaProxy.Core/ImportReview.cs | 1 + .../PortableConfiguration.cs | 3 ++ src/WindowsMegaProxy.Core/ProfileDns.cs | 2 +- src/WindowsMegaProxy.Core/SettingsWriter.cs | 2 +- src/WindowsMegaProxy.Core/TunAddresses.cs | 20 +++++++++++ src/WindowsMegaProxy.Runtime/Engine.cs | 32 +++++++++++++---- src/WindowsMegaProxy/MainWindow.xaml | 21 ++++++++++- src/WindowsMegaProxy/MainWindow.xaml.cs | 5 ++- tests/WindowsMegaProxy.Checks/DnsChecks.cs | 7 ++-- tests/WindowsMegaProxy.Checks/Program.cs | 35 +++++++++++++++++-- tests/WindowsMegaProxy.UiChecks/Program.cs | 5 +++ 17 files changed, 169 insertions(+), 30 deletions(-) diff --git a/assets/strings.json b/assets/strings.json index 7ef6712..d28ac57 100644 --- a/assets/strings.json +++ b/assets/strings.json @@ -1,4 +1,6 @@ { + "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 требует поддержки сервера.", "The imported global SSH fingerprint applies to all SSH profiles. Changing it here updates that global setting.": "Импортированный общий SSH fingerprint применяется ко всем SSH-профилям. Изменение здесь обновляет общую настройку.", "Import notices": "Замечания к импорту", @@ -574,7 +576,7 @@ "TLS fingerprints and bootstrap DNS": "TLS fingerprint и начальный DNS", "Windows DNS": "DNS Windows", "Invalid bootstrap DNS provider.": "Некорректный провайдер начального DNS.", - "Resolves proxy server names directly, before connecting. Automatic uses Cloudflare for proxy-default mode and Windows DNS otherwise. Does not replace application DNS.": "Разрешает имена прокси-серверов напрямую, до подключения. Автоматически: Cloudflare при прокси по умолчанию, иначе DNS Windows. Не заменяет DNS приложений.", + "Resolves proxy server names before connecting. Automatic uses Cloudflare. While connected, ordinary application DNS uses the active profile DNS, including direct traffic. System DNS resumes after disconnection.": "Разрешает имена прокси до подключения. Автоматически используется Cloudflare. Во время подключения обычные DNS-запросы приложений используют DNS активного профиля, включая прямой трафик. После отключения возвращается системный DNS.", "DNS provider": "Провайдер DNS", "Custom DoH URL": "Свой адрес DoH", "Yandex Basic": "Яндекс Базовый", diff --git a/docs/configuration.md b/docs/configuration.md index 107ac57..6d2c6a5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -74,6 +74,7 @@ to fresh settings. MTU and traffic units are portable within this section. | tunMtu | integer 1280–65535; 1500 | | trafficUnits | IEC (default), SI | | bootstrapDnsProvider | auto (default), system, CLOUDFLARE, GOOGLE, QUAD9, YANDEX, YANDEX_SAFE, YANDEX_FAMILY | +| coreLogLevel | trace, debug, info, warn (default), error, fatal, panic; applies after reconnection | | reverseDnsCacheHours | 0 (DNS TTL), 1, 6, 12, 24 (default), 72; failed TTL-mode queries cache for one hour | | resolveConnectionHosts | boolean; false | | showAllConnections | boolean; false | diff --git a/docs/en/profiles-and-routing.md b/docs/en/profiles-and-routing.md index fed1a8a..93185f2 100644 --- a/docs/en/profiles-and-routing.md +++ b/docs/en/profiles-and-routing.md @@ -40,14 +40,25 @@ startup uses cached lists and refreshes them after connecting. ## DNS and connection settings Profile DNS supports Cloudflare, Google, Quad9, Yandex or custom DoH and TCP/UDP -endpoints. When the profile is the default route, its DNS uses the proxy; system -mode retains Windows DNS. UDP DNS needs a UDP-capable transport and does not fall +endpoints. While connected with an active profile, all ordinary application DNS +uses that profile's DNS through the proxy, including applications routed directly. +Traffic rules do not select a separate DNS provider. UDP DNS needs a UDP-capable transport and does not fall back directly. Custom DNS URL query strings are unsupported. Bootstrap DNS resolves proxy hostnames before connecting and can use Windows DNS -or a selected DoH provider directly. Connection settings also include TCP TLS and +or a selected DoH provider directly. Automatic selects Cloudflare. Bootstrap does +not select application DNS. Disconnecting removes the TUN DNS configuration and +returns to the existing Windows DNS settings; physical adapter settings are unchanged. +The Windows DNS cache is cleared at TUN DNS startup and shutdown; applications +with their own DNS caches may need restarting. Without an active profile, system DNS is retained. +Application-managed DoH is unaffected. Connection settings also include TCP TLS and SSH fingerprint presets; H3 uses the core's native fingerprint. +Connection settings include the sing-box log level (default `warn`). Changes are +saved automatically and apply after reconnection. Use `debug` for troubleshooting; +`trace` is more verbose. These logs include destination addresses and application +paths and use the existing local log rotation limit. + ## Configuration transfer and subscriptions Import MegaProxy JSON v1–8, FoxyProxy JSON, ProxyList or SuperProxy. Export MegaProxy diff --git a/docs/ru/profiles-and-routing.md b/docs/ru/profiles-and-routing.md index 01ecbbd..3838c6d 100644 --- a/docs/ru/profiles-and-routing.md +++ b/docs/ru/profiles-and-routing.md @@ -39,14 +39,25 @@ SSH и HTTPS только через TCP не предоставляют пер ## DNS и параметры подключения DNS профиля поддерживает Cloudflare, Google, Quad9, Yandex, собственный DoH -и TCP/UDP-серверы. Когда профиль — маршрут по умолчанию, его DNS идёт через прокси; -системный режим сохраняет DNS Windows. Для UDP DNS нужен совместимый транспорт, +и TCP/UDP-серверы. При подключении с активным профилем все обычные DNS-запросы +приложений идут через DNS этого профиля и прокси, включая приложения с прямым +маршрутом. Правила трафика не выбирают отдельный DNS. Для UDP DNS нужен совместимый транспорт, прямого резервного маршрута нет. Query-параметры в DNS URL не поддерживаются. Bootstrap DNS разрешает имя прокси до подключения через DNS Windows или выбранный -DoH напрямую. Доступны TCP TLS и SSH fingerprint; для H3 используется штатный +DoH напрямую. Автоматический выбор означает Cloudflare. Bootstrap не выбирает DNS +приложений. При отключении удаляется настройка DNS туннеля и возвращается системный +DNS; настройки физических адаптеров не меняются. При включении и отключении DNS +туннеля очищается кеш DNS Windows; приложения со своим кешем может потребоваться +перезапустить. Без активного профиля сохраняется системный DNS. +Собственный DoH приложений не меняется. Доступны TCP TLS и SSH fingerprint; для H3 используется штатный fingerprint ядра. +В настройках подключения выбирается уровень логов sing-box (по умолчанию `warn`). +Выбор сохраняется автоматически и применяется после переподключения. Для диагностики +используйте `debug`; `trace` ещё подробнее. Журнал содержит адреса назначения и пути +приложений, его размер ограничивается существующей настройкой ротации. + ## Конфигурации, подписки и диагностика Импорт: MegaProxy JSON v1–8, FoxyProxy JSON, ProxyList и SuperProxy. diff --git a/src/WindowsMegaProxy.Core/Configuration.cs b/src/WindowsMegaProxy.Core/Configuration.cs index 8539c20..18619e0 100644 --- a/src/WindowsMegaProxy.Core/Configuration.cs +++ b/src/WindowsMegaProxy.Core/Configuration.cs @@ -32,6 +32,7 @@ public sealed class Settings public string Theme { get; set; } = "auto"; public bool AlwaysMinimizeToTray { get; set; } public string TlsFingerprint { get; set; } = "DEFAULT"; + public string CoreLogLevel { get; set; } = "warn"; public string BootstrapDnsProvider { get; set; } = "auto"; public int TunMtu { get; set; } = 1500; public string TrafficUnits { get; set; } = "IEC"; @@ -101,7 +102,13 @@ public static class Configuration "FIREFOX_ANDROID" => "firefox", "EDGE_ANDROID" => "edge", "RANDOMIZED" => "randomized", _ => "chrome" }; public static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - public static bool UsesProxy(Settings settings) => settings.DefaultAction == "proxy" || AppUpdatePolicy.ThroughProxy(settings) || + public static void ValidateCoreLogLevel(string value) + { + if (value is not ("trace" or "debug" or "info" or "warn" or "error" or "fatal" or "panic")) + throw new ArgumentException("Invalid sing-box log level."); + } + + public static bool UsesProxy(Settings settings) => settings.ActiveProfile is not null || settings.DefaultAction == "proxy" || AppUpdatePolicy.ThroughProxy(settings) || settings.Rules.Any(rule => rule.Enabled && (rule.Action == "proxy" || rule.PodkopThroughProxy && PodkopCatalog.Parse(rule.Destination.Trim()).Length > 0)); public static bool RequiresHttp3(Settings settings) => UsesProxy(settings) && settings.ActiveProfile?.UsesHttp3 == true; public static bool RequiresAlpha(Settings settings) => RequiresHttp3(settings) || UsesProxy(settings) && settings.ActiveProfile?.PreferHttp2 == true; @@ -137,7 +144,9 @@ public static string FolderPattern(string folder) public static JsonObject Build(Settings settings, int apiPort, string apiSecret, bool allowUnresolvedSsh = false) { ValidateAction(settings.DefaultAction); + ValidateCoreLogLevel(settings.CoreLogLevel); bool proxyDefault = settings.DefaultAction == "proxy"; + bool interceptDns = settings.ActiveProfile is not null; var outbounds = new JsonArray(new JsonObject { ["type"] = "direct", ["tag"] = "direct" }); if (UsesProxy(settings)) { @@ -152,7 +161,7 @@ public static JsonObject Build(Settings settings, int apiPort, string apiSecret, var rules = new JsonArray { - proxyDefault ? new JsonObject { ["port"] = 53, ["action"] = "hijack-dns" } + interceptDns ? new JsonObject { ["port"] = 53, ["action"] = "hijack-dns" } : new JsonObject { ["port"] = 53, ["action"] = "route", ["outbound"] = "direct" }, new JsonObject { ["process_name"] = new JsonArray(DirectProcessNames.Select(name => (JsonNode)JsonValue.Create(name)!).ToArray()), ["action"] = "route", ["outbound"] = "direct" } }; @@ -183,27 +192,27 @@ public static JsonObject Build(Settings settings, int apiPort, string apiSecret, return new JsonObject { - ["log"] = new JsonObject { ["level"] = "warn", ["timestamp"] = true }, + ["log"] = new JsonObject { ["level"] = settings.CoreLogLevel, ["timestamp"] = true }, ["dns"] = new JsonObject { - ["servers"] = proxyDefault ? new JsonArray( + ["servers"] = interceptDns ? new JsonArray( ProfileDns.BuildBootstrap(settings.BootstrapDnsProvider, proxyDefault), ProfileDns.Build(settings.ActiveProfile!)) : new JsonArray(ProfileDns.BuildBootstrap(settings.BootstrapDnsProvider, proxyDefault), new JsonObject { ["type"] = "local", ["tag"] = "system" }), - ["final"] = proxyDefault ? "dns" : "system", + ["final"] = interceptDns ? "dns" : "system", ["strategy"] = "prefer_ipv4", ["reverse_mapping"] = true }, ["inbounds"] = new JsonArray(new JsonObject { ["type"] = "tun", ["tag"] = "tun", ["interface_name"] = AppIdentity.DisplayName, ["address"] = new JsonArray(tunAddresses.Ipv4, tunAddresses.Ipv6), - ["mtu"] = settings.TunMtu, ["auto_route"] = true, ["strict_route"] = proxyDefault, - ["dns_mode"] = proxyDefault ? "hijack" : "disabled" + ["mtu"] = settings.TunMtu, ["auto_route"] = true, ["strict_route"] = interceptDns, + ["dns_mode"] = interceptDns ? "hijack" : "disabled" }), ["outbounds"] = outbounds, ["route"] = new JsonObject { - ["auto_detect_interface"] = true, ["find_process"] = true, ["default_domain_resolver"] = proxyDefault ? "bootstrap" : "system", ["rules"] = rules, + ["auto_detect_interface"] = true, ["find_process"] = true, ["default_domain_resolver"] = interceptDns ? "bootstrap" : "system", ["rules"] = rules, ["rule_set"] = new JsonArray(PodkopCatalog.Groups(settings).Select(ids => (JsonNode)PodkopCatalog.Definition(ids)).ToArray()), ["final"] = proxyDefault ? "proxy" : "direct" }, diff --git a/src/WindowsMegaProxy.Core/ConfigurationFields.cs b/src/WindowsMegaProxy.Core/ConfigurationFields.cs index 1182441..e96efbd 100644 --- a/src/WindowsMegaProxy.Core/ConfigurationFields.cs +++ b/src/WindowsMegaProxy.Core/ConfigurationFields.cs @@ -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 bootstrapDnsProvider launchAtSignIn rules tunMtu trafficUnits automaticAppUpdates appUpdatesThroughProxy"); + Keep(windows, "version defaultAction bypassLocalNetworks autoConnect resetExistingTcpConnections logSizeMb reverseDnsCacheHours resolveConnectionHosts showAllConnections language theme alwaysMinimizeToTray tlsFingerprint coreLogLevel bootstrapDnsProvider launchAtSignIn rules tunMtu trafficUnits automaticAppUpdates appUpdatesThroughProxy"); Fingerprint(windows, "tlsFingerprint"); } } diff --git a/src/WindowsMegaProxy.Core/ImportReview.cs b/src/WindowsMegaProxy.Core/ImportReview.cs index 66b3475..3224df1 100644 --- a/src/WindowsMegaProxy.Core/ImportReview.cs +++ b/src/WindowsMegaProxy.Core/ImportReview.cs @@ -127,6 +127,7 @@ 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.CoreLogLevel = current.CoreLogLevel; result.ResetExistingTcpConnections = current.ResetExistingTcpConnections; result.WindowsOptions = (JsonObject)current.WindowsOptions.DeepClone(); result.Rules = current.Rules.Select(rule => rule.Copy()).ToList(); diff --git a/src/WindowsMegaProxy.Core/PortableConfiguration.cs b/src/WindowsMegaProxy.Core/PortableConfiguration.cs index b868143..829d9c6 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("coreLogLevel")) candidate.CoreLogLevel = String(windows, "coreLogLevel"); if (windows.ContainsKey("bootstrapDnsProvider")) candidate.BootstrapDnsProvider = String(windows, "bootstrapDnsProvider"); if (windows.ContainsKey("launchAtSignIn")) candidate.LaunchAtSignIn = windows["launchAtSignIn"]!.GetValue(); if (windows.ContainsKey("rules")) candidate.Rules = windows["rules"]!.Deserialize>(PortableOptions) ?? throw Invalid(Ui.T("Invalid Windows rules.")); @@ -158,6 +159,7 @@ public static JsonObject Export(Settings settings, bool includePasswords = false windows["theme"] = settings.Theme; windows["alwaysMinimizeToTray"] = settings.AlwaysMinimizeToTray; windows["bootstrapDnsProvider"] = settings.BootstrapDnsProvider; + windows["coreLogLevel"] = settings.CoreLogLevel; windows["launchAtSignIn"] = settings.LaunchAtSignIn ?? false; windows["rules"] = JsonSerializer.SerializeToNode(settings.Rules, PortableOptions); } @@ -190,6 +192,7 @@ public static void ValidateSettings(Settings settings) if (settings.Language is not ("en" or "ru") || settings.Theme is not ("auto" or "light" or "dark")) throw Invalid("Invalid language or theme."); settings.TlsFingerprint = ConfigurationFields.NormalizeFingerprint(settings.TlsFingerprint); if (settings.ReverseDnsCacheHours is not (0 or 1 or 6 or 12 or 24 or 72)) throw Invalid("Unsupported reverse DNS cache duration."); + Configuration.ValidateCoreLogLevel(settings.CoreLogLevel); _ = ProfileDns.BuildBootstrap(settings.BootstrapDnsProvider, false); _ = ConfigurationSubscription.Get(settings); _ = AppUpdatePolicy.Enabled(settings); diff --git a/src/WindowsMegaProxy.Core/ProfileDns.cs b/src/WindowsMegaProxy.Core/ProfileDns.cs index e12b42d..4341baa 100644 --- a/src/WindowsMegaProxy.Core/ProfileDns.cs +++ b/src/WindowsMegaProxy.Core/ProfileDns.cs @@ -23,7 +23,7 @@ public static readonly (string Id, string Name, string Url)[] Providers = public static JsonObject BuildBootstrap(string provider, bool proxyDefault) { - if (provider == "auto") provider = proxyDefault ? "CLOUDFLARE" : "system"; + if (provider == "auto") provider = "CLOUDFLARE"; if (provider == "system") return new JsonObject { ["type"] = "local", ["tag"] = "bootstrap" }; string address = provider switch { diff --git a/src/WindowsMegaProxy.Core/SettingsWriter.cs b/src/WindowsMegaProxy.Core/SettingsWriter.cs index c7b7376..2bd3e5f 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.TunMtu == b.TunMtu && a.TlsFingerprint == b.TlsFingerprint && a.BootstrapDnsProvider == b.BootstrapDnsProvider && a.ActiveProfileId == b.ActiveProfileId && + 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 88ebe96..b3133e4 100644 --- a/src/WindowsMegaProxy.Core/TunAddresses.cs +++ b/src/WindowsMegaProxy.Core/TunAddresses.cs @@ -3,11 +3,31 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Text.Json.Nodes; 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 void Apply(JsonObject config, (string Ipv4, string Ipv6) addresses, bool ipv6) + { + config["inbounds"]![0]!["address"] = ipv6 ? 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 + { + ["ip_version"] = 6, ["action"] = "reject" + }); + } + public static IPNetwork Network(IPAddress address, int prefixLength) { byte[] bytes = address.GetAddressBytes(); diff --git a/src/WindowsMegaProxy.Runtime/Engine.cs b/src/WindowsMegaProxy.Runtime/Engine.cs index b747fa6..66611b8 100644 --- a/src/WindowsMegaProxy.Runtime/Engine.cs +++ b/src/WindowsMegaProxy.Runtime/Engine.cs @@ -7,6 +7,7 @@ using System.Net.Http.Headers; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Nodes; @@ -17,6 +18,10 @@ namespace WindowsMegaProxy; public sealed partial class Engine : IEngine { + [DllImport("dnsapi.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DnsFlushResolverCache(); + private bool tunDnsActive; private Process? process; private HttpClient client = new(new HttpClientHandler { UseProxy = false }) { Timeout = TimeSpan.FromSeconds(2) }; private readonly string stateDirectory; @@ -141,16 +146,21 @@ private async Task StartCoreAsync(Settings settings, bool testOnly, Cancellation var config = Configuration.Build(settings, port, secret); if (!testOnly) { - var addresses = await Task.Run(() => + var network = await Task.Run(() => { - var networks = NetworkInterface.GetAllNetworkInterfaces() + var adapters = NetworkInterface.GetAllNetworkInterfaces() .Where(adapter => !string.Equals(adapter.Name, AppIdentity.DisplayName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(adapter => adapter.GetIPProperties().UnicastAddresses) + .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))); + var networks = adapters.SelectMany(adapter => adapter.GetIPProperties().UnicastAddresses) .Select(address => TunAddresses.Network(address.Address, address.PrefixLength)); - return TunAddresses.Select(networks, AppIdentity.IsDev); + return (Addresses: TunAddresses.Select(networks, AppIdentity.IsDev), Ipv6: ipv6); }, cancellationToken).ConfigureAwait(false); - config["inbounds"]![0]!["address"] = new JsonArray(addresses.Ipv4, addresses.Ipv6); - Log?.Invoke($"TUN interface {AppIdentity.DisplayName}: IPv4={addresses.Ipv4}; IPv6={addresses.Ipv6}."); + 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)")}."); } var cache = new PodkopCache(Path.Combine(stateDirectory, "podkop"), message => Log?.Invoke(message)); foreach (string id in PodkopCatalog.Used(settings)) await cache.GetAsync(id, cancellationToken, @@ -192,6 +202,7 @@ private async Task StartCoreAsync(Settings settings, bool testOnly, Cancellation ? await Task.Run(() => ExistingTcpConnections.Capture(settings, message => Log?.Invoke(message)), cancellationToken) : []; process = Process.Start(StartInfo(executable)) ?? throw new InvalidOperationException("The core did not start."); + tunDnsActive = !testOnly && config["inbounds"]![0]!["dns_mode"]?.ToString() == "hijack"; job.Assign(process); process.EnableRaisingEvents = true; process.Exited += (_, _) => { if (!changingProfile) Exited?.Invoke(); }; @@ -206,6 +217,9 @@ private async Task StartCoreAsync(Settings settings, bool testOnly, Cancellation if (response.IsSuccessStatusCode) { cancellationToken.ThrowIfCancellationRequested(); + if (!testOnly && config["inbounds"]![0]!["dns_mode"]?.ToString() == "hijack") + Log?.Invoke(DnsFlushResolverCache() ? "Windows DNS cache cleared after TUN DNS activation." + : $"Windows DNS cache could not be cleared (error {Marshal.GetLastWin32Error()}). Applications may retain stale DNS records."); if (existing.Length > 0) await Task.Run(() => ExistingTcpConnections.Close(existing, message => Log?.Invoke(message)), cancellationToken); return; @@ -342,6 +356,12 @@ private async Task StopSelectedAsync() finally { process?.Dispose(); process = null; + if (tunDnsActive) + { + tunDnsActive = false; + Log?.Invoke(DnsFlushResolverCache() ? "Windows DNS cache cleared after TUN shutdown; system DNS restored." + : $"Windows DNS cache could not be cleared after TUN shutdown (error {Marshal.GetLastWin32Error()})."); + } } } Log?.Invoke("Network core stopped."); diff --git a/src/WindowsMegaProxy/MainWindow.xaml b/src/WindowsMegaProxy/MainWindow.xaml index 1445641..9215b63 100644 --- a/src/WindowsMegaProxy/MainWindow.xaml +++ b/src/WindowsMegaProxy/MainWindow.xaml @@ -1261,7 +1261,7 @@ @@ -1275,6 +1275,25 @@ +