/* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using DnsServerCore.Auth; using DnsServerCore.Cluster; using DnsServerCore.Dhcp; using DnsServerCore.Dns; using DnsServerCore.Dns.Applications; using DnsServerCore.Dns.Dnssec; using DnsServerCore.Dns.Zones; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Quic; using System.Net.Security; using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ClientConnection; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore { public sealed partial class DnsWebService : IAsyncDisposable, IDisposable { #region variables readonly static char[] commaSeparator = new char[] { ',' }; readonly Version _currentVersion; readonly DateTime _uptimestamp = DateTime.UtcNow; readonly string _appFolder; readonly string _configFolder; readonly LogManager _log; readonly AuthManager _authManager; readonly WebServiceApi _api; readonly WebServiceDashboardApi _dashboardApi; readonly WebServiceZonesApi _zonesApi; readonly WebServiceOtherZonesApi _otherZonesApi; readonly WebServiceAppsApi _appsApi; readonly WebServiceSettingsApi _settingsApi; readonly WebServiceDhcpApi _dhcpApi; readonly WebServiceAuthApi _authApi; readonly WebServiceClusterApi _clusterApi; readonly WebServiceLogsApi _logsApi; WebApplication _webService; ClusterManager _clusterManager; DnsServer _dnsServer; DhcpServer _dhcpServer; //web service IReadOnlyList _webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any]; int _webServiceHttpPort = 5380; int _webServiceTlsPort = 53443; bool _webServiceEnableTls; bool _webServiceEnableHttp3; bool _webServiceHttpToTlsRedirect; bool _webServiceUseSelfSignedTlsCertificate; string _webServiceTlsCertificatePath; string _webServiceTlsCertificatePassword; string _webServiceRealIpHeader = "X-Real-IP"; Timer _tlsCertificateUpdateTimer; const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000; const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000; DateTime _webServiceCertificateLastModifiedOn; SslServerAuthenticationOptions _webServiceSslServerAuthenticationOptions; List _configDisabledZones; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; bool _isRunning; #endregion #region constructor public DnsWebService(string configFolder = null, Uri updateCheckUri = null) { Assembly assembly = Assembly.GetExecutingAssembly(); _currentVersion = assembly.GetName().Version; _appFolder = Path.GetDirectoryName(assembly.Location); if (configFolder is null) _configFolder = Path.Combine(_appFolder, "config"); else _configFolder = configFolder; Directory.CreateDirectory(_configFolder); Directory.CreateDirectory(Path.Combine(_configFolder, "blocklists")); Directory.CreateDirectory(Path.Combine(_configFolder, "zones")); _log = new LogManager(_configFolder); _authManager = new AuthManager(_configFolder, _log); _api = new WebServiceApi(this, updateCheckUri); _dashboardApi = new WebServiceDashboardApi(this); _zonesApi = new WebServiceZonesApi(this); _otherZonesApi = new WebServiceOtherZonesApi(this); _appsApi = new WebServiceAppsApi(this); _settingsApi = new WebServiceSettingsApi(this); _dhcpApi = new WebServiceDhcpApi(this); _authApi = new WebServiceAuthApi(this); _clusterApi = new WebServiceClusterApi(this); _logsApi = new WebServiceLogsApi(this); _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { if (_pendingSave) { try { SaveConfigFileInternal(); _pendingSave = false; } catch (Exception ex) { _log.Write(ex); //set timer to retry again _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } } }); } #endregion #region IDisposable bool _disposed; public async ValueTask DisposeAsync() { if (_disposed) return; StopTlsCertificateUpdateTimer(); lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveConfigFileInternal(); } catch (Exception ex) { _log.Write(ex); } finally { _pendingSave = false; } } } await StopAsync(); _authManager?.Dispose(); _log?.Dispose(); _disposed = true; } public void Dispose() { DisposeAsync().Sync(); } #endregion #region config private void LoadConfigFile() { string webServiceConfigFile = Path.Combine(_configFolder, "webservice.config"); try { using (FileStream fS = new FileStream(webServiceConfigFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS); } _log.Write("Web Service config file was loaded: " + webServiceConfigFile); } catch (FileNotFoundException) { if (!TryLoadOldConfigFile()) { //old config file did not exist; read environment variables and generate new config CreateForwarderZoneToDisableDnssecForNTP(); //web service string strWebServiceLocalAddresses = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_LOCAL_ADDRESSES"); if (!string.IsNullOrEmpty(strWebServiceLocalAddresses)) _webServiceLocalAddresses = strWebServiceLocalAddresses.Split(IPAddress.Parse, commaSeparator); string strWebServiceHttpPort = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_HTTP_PORT"); if (!string.IsNullOrEmpty(strWebServiceHttpPort)) _webServiceHttpPort = int.Parse(strWebServiceHttpPort); string webServiceTlsPort = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_HTTPS_PORT"); if (!string.IsNullOrEmpty(webServiceTlsPort)) _webServiceTlsPort = int.Parse(webServiceTlsPort); UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort]; string webServiceEnableTls = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS"); if (!string.IsNullOrEmpty(webServiceEnableTls)) _webServiceEnableTls = bool.Parse(webServiceEnableTls); string webServiceTlsCertificatePassword = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD"); if (!string.IsNullOrEmpty(webServiceTlsCertificatePassword)) _webServiceTlsCertificatePassword = webServiceTlsCertificatePassword; string webServiceTlsCertificatePath = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH"); if (!string.IsNullOrEmpty(webServiceTlsCertificatePath)) { _webServiceTlsCertificatePath = webServiceTlsCertificatePath; string webServiceTlsCertificateAbsolutePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath); try { LoadWebServiceTlsCertificate(webServiceTlsCertificateAbsolutePath, _webServiceTlsCertificatePassword); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading Web Service TLS certificate: " + webServiceTlsCertificateAbsolutePath + "\r\n" + ex.ToString()); } StartTlsCertificateUpdateTimer(); } string webServiceUseSelfSignedTlsCertificate = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT"); if (!string.IsNullOrEmpty(webServiceUseSelfSignedTlsCertificate)) { _webServiceUseSelfSignedTlsCertificate = bool.Parse(webServiceUseSelfSignedTlsCertificate); if (_webServiceUseSelfSignedTlsCertificate && !File.Exists(Path.Combine(_configFolder, "dns.config"))) { //read DNS server domain name here to generate self signed cert string serverDomain = Environment.GetEnvironmentVariable("DNS_SERVER_DOMAIN"); if (!string.IsNullOrEmpty(serverDomain)) _dnsServer.ServerDomain = serverDomain; } CheckAndLoadSelfSignedCertificate(false, false); } string webServiceHttpToTlsRedirect = Environment.GetEnvironmentVariable("DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT"); if (!string.IsNullOrEmpty(webServiceHttpToTlsRedirect)) _webServiceHttpToTlsRedirect = bool.Parse(webServiceHttpToTlsRedirect); } SaveConfigFileInternal(); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading Web Service config file: " + webServiceConfigFile + "\r\n" + ex.ToString()); _log.Write("Note: You may try deleting the Web Service config file to fix this issue. However, you will lose Web Service settings but, other data wont be affected."); throw; } } public void LoadConfig(Stream s) { lock (_saveLock) { ReadConfigFrom(s); SaveConfigFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void CreateForwarderZoneToDisableDnssecForNTP() { if (Environment.OSVersion.Platform == PlatformID.Unix) { //adding a conditional forwarder zone for disabling DNSSEC validation for ntp.org so that systems with no real-time clock can sync time string ntpDomain = "ntp.org"; string fwdRecordComments = "This forwarder zone was automatically created to disable DNSSEC validation for ntp.org to allow systems with no real-time clock (e.g. Raspberry Pi) to sync time via NTP when booting."; if (_dnsServer.AuthZoneManager.CreateForwarderZone(ntpDomain, DnsTransportProtocol.Udp, "this-server", false, DnsForwarderRecordProxyType.DefaultProxy, null, 0, null, null, fwdRecordComments) is not null) { //set permissions _authManager.SetPermission(PermissionSection.Zones, ntpDomain, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, ntpDomain, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); } } } private void SaveConfigFileInternal() { string configFile = Path.Combine(_configFolder, "webservice.config"); using (MemoryStream mS = new MemoryStream()) { //serialize config WriteConfigTo(mS); //write config mS.Position = 0; using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write)) { mS.CopyTo(fS); } } _log.Write("Web Service config file was saved: " + configFile); } public void SaveConfigFile() { lock (_saveLock) { if (_pendingSave) return; _pendingSave = true; _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void InspectAndFixZonePermissions() { Permission permission = _authManager.GetPermission(PermissionSection.Zones); if (permission is null) throw new DnsWebServiceException("Failed to read 'Zones' permissions: auth.config file is probably corrupt."); IReadOnlyDictionary subItemPermissions = permission.SubItemPermissions; //remove ghost permissions foreach (KeyValuePair subItemPermission in subItemPermissions) { string zoneName = subItemPermission.Key; if (_dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName) is null) permission.RemoveAllSubItemPermissions(zoneName); //no such zone exists; remove permissions } //add missing admin permissions IReadOnlyList zones = _dnsServer.AuthZoneManager.GetAllZones(); Group admins = _authManager.GetGroup(Group.ADMINISTRATORS); if (admins is null) throw new DnsWebServiceException("Failed to find 'Administrators' group: auth.config file is probably corrupt."); Group dnsAdmins = _authManager.GetGroup(Group.DNS_ADMINISTRATORS); if (dnsAdmins is null) throw new DnsWebServiceException("Failed to find 'DNS Administrators' group: auth.config file is probably corrupt."); foreach (AuthZoneInfo zone in zones) { if (zone.Internal) { _authManager.SetPermission(PermissionSection.Zones, zone.Name, admins, PermissionFlag.View); _authManager.SetPermission(PermissionSection.Zones, zone.Name, dnsAdmins, PermissionFlag.View); } else { _authManager.SetPermission(PermissionSection.Zones, zone.Name, admins, PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, zone.Name, dnsAdmins, PermissionFlag.ViewModifyDelete); } } _authManager.SaveConfigFile(); } private void ReadConfigFrom(Stream s) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "WC") //format throw new InvalidDataException("Web Service config file format is invalid."); int version = bR.ReadByte(); if (version > 1) throw new InvalidDataException("Web Service config version not supported."); _webServiceHttpPort = bR.ReadInt32(); _webServiceTlsPort = bR.ReadInt32(); { IPAddress[] webServiceLocalAddresses; int count = bR.ReadByte(); if (count > 0) { IPAddress[] localAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) localAddresses[i] = IPAddressExtensions.ReadFrom(bR); webServiceLocalAddresses = localAddresses; } else { webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any]; } _webServiceLocalAddresses = webServiceLocalAddresses; } _webServiceEnableTls = bR.ReadBoolean(); _webServiceEnableHttp3 = bR.ReadBoolean(); _webServiceHttpToTlsRedirect = bR.ReadBoolean(); _webServiceUseSelfSignedTlsCertificate = bR.ReadBoolean(); _webServiceTlsCertificatePath = bR.ReadShortString(); _webServiceTlsCertificatePassword = bR.ReadShortString(); if (_webServiceTlsCertificatePath.Length == 0) _webServiceTlsCertificatePath = null; if (_webServiceTlsCertificatePath is null) { StopTlsCertificateUpdateTimer(); } else { string webServiceTlsCertificateAbsolutePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath); try { LoadWebServiceTlsCertificate(webServiceTlsCertificateAbsolutePath, _webServiceTlsCertificatePassword); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading Web Service TLS certificate: " + webServiceTlsCertificateAbsolutePath + "\r\n" + ex.ToString()); } StartTlsCertificateUpdateTimer(); } CheckAndLoadSelfSignedCertificate(false, false); _webServiceRealIpHeader = bR.ReadShortString(); } private void WriteConfigTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("WC")); //format bW.Write((byte)1); //version bW.Write(_webServiceHttpPort); bW.Write(_webServiceTlsPort); { bW.Write(Convert.ToByte(_webServiceLocalAddresses.Count)); foreach (IPAddress localAddress in _webServiceLocalAddresses) localAddress.WriteTo(bW); } bW.Write(_webServiceEnableTls); bW.Write(_webServiceEnableHttp3); bW.Write(_webServiceHttpToTlsRedirect); bW.Write(_webServiceUseSelfSignedTlsCertificate); if (_webServiceTlsCertificatePath is null) bW.WriteShortString(string.Empty); else bW.WriteShortString(_webServiceTlsCertificatePath); if (_webServiceTlsCertificatePassword is null) bW.WriteShortString(string.Empty); else bW.WriteShortString(_webServiceTlsCertificatePassword); bW.WriteShortString(_webServiceRealIpHeader); } #endregion #region backup and restore config internal async Task BackupConfigAsync(Stream zipStream, bool authConfig, bool clusterConfig, bool webServiceSettings, bool dnsSettings, bool logSettings, bool zones, bool allowedZones, bool blockedZones, bool blockLists, bool apps, bool scopes, bool stats, bool logs, bool isConfigTransfer = false, DateTime ifModifiedSince = default, IReadOnlyCollection includeZones = null) { using (ZipArchive backupZip = new ZipArchive(zipStream, ZipArchiveMode.Create, true, Encoding.UTF8)) { if (authConfig) { string authConfigFile = Path.Combine(_configFolder, "auth.config"); if (File.Exists(authConfigFile) && (File.GetLastWriteTimeUtc(authConfigFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(authConfigFile, "auth.config"); } if (clusterConfig && !isConfigTransfer) { string clusterConfigFile = Path.Combine(_configFolder, "cluster.config"); if (File.Exists(clusterConfigFile)) backupZip.CreateEntryFromFile(clusterConfigFile, "cluster.config"); } if (webServiceSettings && !isConfigTransfer) { string webServiceConfigFile = Path.Combine(_configFolder, "webservice.config"); if (File.Exists(webServiceConfigFile) && (File.GetLastWriteTimeUtc(webServiceConfigFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(webServiceConfigFile, "webservice.config"); //backup web service cert if (!isConfigTransfer && !string.IsNullOrEmpty(_webServiceTlsCertificatePath)) { string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath); if (File.Exists(webServiceTlsCertificatePath) && webServiceTlsCertificatePath.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) { string entryName = ConvertToRelativePath(webServiceTlsCertificatePath).Replace('\\', '/'); backupZip.CreateEntryFromFile(webServiceTlsCertificatePath, entryName); } } } if (dnsSettings) { string dnsConfigFile = Path.Combine(_configFolder, "dns.config"); if (File.Exists(dnsConfigFile) && (File.GetLastWriteTimeUtc(dnsConfigFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(dnsConfigFile, "dns.config"); //backup optional protocols cert if (!isConfigTransfer && !string.IsNullOrEmpty(_dnsServer.DnsTlsCertificatePath)) { string dnsTlsCertificatePath = ConvertToAbsolutePath(_dnsServer.DnsTlsCertificatePath); if (File.Exists(dnsTlsCertificatePath) && dnsTlsCertificatePath.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) { string entryName = ConvertToRelativePath(dnsTlsCertificatePath).Replace('\\', '/'); backupZip.CreateEntryFromFile(dnsTlsCertificatePath, entryName); } } } if (logSettings && !isConfigTransfer) { string logConfigFile = Path.Combine(_configFolder, "log.config"); if (File.Exists(logConfigFile) && (File.GetLastWriteTimeUtc(logConfigFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(logConfigFile, "log.config"); } if (zones) { if (isConfigTransfer) { //backup Primary zone DNSSEC private keys that are member zone of the cluster catalog zone AuthZoneInfo clusterCatalogZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo("cluster-catalog." + _clusterManager.ClusterDomain); if ((clusterCatalogZoneInfo is not null) && (clusterCatalogZoneInfo.Type == AuthZoneType.Catalog)) { IReadOnlyCollection memberZoneNames = (clusterCatalogZoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames(); foreach (string memberZoneName in memberZoneNames) { AuthZoneInfo memberZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; //no such zone exists; ignore if (memberZoneInfo.Type != AuthZoneType.Primary) continue; //not a Primary zone; ignore if (memberZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned) continue; //not a DNSSEC signed zone; ignore IReadOnlyCollection dnssecPrivateKeys = memberZoneInfo.DnssecPrivateKeys; bool includePrivateKeys = false; if ((includeZones is not null) && includeZones.Contains(memberZoneInfo.Name)) { includePrivateKeys = true; } else { foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) { if (dnssecPrivateKey.StateChangedOn > ifModifiedSince) { //found a changed key includePrivateKeys = true; break; } } } if (includePrivateKeys) { using (MemoryStream mS = new MemoryStream(4096)) { AuthZoneInfo.WriteDnssecPrivateKeysTo(dnssecPrivateKeys, new BinaryWriter(mS)); mS.Position = 0; //create zip entry ZipArchiveEntry entry = backupZip.CreateEntry("zones/" + memberZoneName + ".keys", CompressionLevel.Optimal); await using (Stream entryStream = entry.Open()) { await mS.CopyToAsync(entryStream); } } } } } } else { //backup zone files string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly); foreach (string zoneFile in zoneFiles) { string entryName = "zones/" + Path.GetFileName(zoneFile); backupZip.CreateEntryFromFile(zoneFile, entryName); } } } if (allowedZones) { string allowedZonesFile = Path.Combine(_configFolder, "allowed.config"); if (File.Exists(allowedZonesFile) && (File.GetLastWriteTimeUtc(allowedZonesFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(allowedZonesFile, "allowed.config"); } if (blockedZones) { string blockedZonesFile = Path.Combine(_configFolder, "blocked.config"); if (File.Exists(blockedZonesFile) && (File.GetLastWriteTimeUtc(blockedZonesFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(blockedZonesFile, "blocked.config"); } if (blockLists) { string blockListConfigFile = Path.Combine(_configFolder, "blocklist.config"); if (File.Exists(blockListConfigFile) && (File.GetLastWriteTimeUtc(blockListConfigFile) > ifModifiedSince)) backupZip.CreateEntryFromFile(blockListConfigFile, "blocklist.config"); string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly); foreach (string blockListFile in blockListFiles) { if (File.GetLastWriteTimeUtc(blockListFile) > ifModifiedSince) { string entryName = "blocklists/" + Path.GetFileName(blockListFile); backupZip.CreateEntryFromFile(blockListFile, entryName); } } } if (apps) { if (isConfigTransfer) { string[] appDirectories = Directory.GetDirectories(Path.Combine(_configFolder, "apps"), "*", SearchOption.TopDirectoryOnly); foreach (string appDirectory in appDirectories) { string applicationName = Path.GetFileName(appDirectory); string applicationZipFile = Path.Combine(appDirectory, applicationName + ".zip"); string configFile = Path.Combine(appDirectory, "dnsApp.config"); bool fileAdded = false; if (File.Exists(applicationZipFile) && (File.GetLastWriteTimeUtc(applicationZipFile) > ifModifiedSince)) { string entryName = "apps/" + applicationName + "/" + applicationName + ".zip"; backupZip.CreateEntryFromFile(applicationZipFile, entryName); fileAdded = true; } if (File.Exists(configFile) && (File.GetLastWriteTimeUtc(configFile) > ifModifiedSince)) { string entryName = "apps/" + applicationName + "/dnsApp.config"; backupZip.CreateEntryFromFile(configFile, entryName); fileAdded = true; } if (!fileAdded) _ = backupZip.CreateEntry("apps/" + applicationName + "/.exists", CompressionLevel.Optimal); } } else { string[] appFiles = Directory.GetFiles(Path.Combine(_configFolder, "apps"), "*", SearchOption.AllDirectories); foreach (string appFile in appFiles) { string entryName = appFile.Substring(_configFolder.Length); if (Path.DirectorySeparatorChar != '/') entryName = entryName.Replace(Path.DirectorySeparatorChar, '/'); entryName = entryName.TrimStart('/'); await CreateBackupEntryFromSharedFileAsync(backupZip, appFile, entryName); } } } if (scopes && !isConfigTransfer) { string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly); foreach (string scopeFile in scopeFiles) { string entryName = "scopes/" + Path.GetFileName(scopeFile); backupZip.CreateEntryFromFile(scopeFile, entryName); } } if (stats && !isConfigTransfer) { string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly); foreach (string hourlyStatsFile in hourlyStatsFiles) { string entryName = "stats/" + Path.GetFileName(hourlyStatsFile); backupZip.CreateEntryFromFile(hourlyStatsFile, entryName); } string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly); foreach (string dailyStatsFile in dailyStatsFiles) { string entryName = "stats/" + Path.GetFileName(dailyStatsFile); backupZip.CreateEntryFromFile(dailyStatsFile, entryName); } } if (logs && !isConfigTransfer) { string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly); foreach (string logFile in logFiles) { string entryName = "logs/" + Path.GetFileName(logFile); if (logFile.Equals(_log.CurrentLogFile, StringComparison.OrdinalIgnoreCase)) { await CreateBackupEntryFromSharedFileAsync(backupZip, logFile, entryName); } else { backupZip.CreateEntryFromFile(logFile, entryName); } } } } } internal async Task RestoreConfigAsync(Stream zipStream, bool authConfig, bool clusterConfig, bool webServiceSettings, bool dnsSettings, bool logSettings, bool zones, bool allowedZones, bool blockedZones, bool blockLists, bool apps, bool scopes, bool stats, bool logs, bool deleteExistingFiles, UserSession implantSession = null, bool isConfigTransfer = false) { using (ZipArchive backupZip = new ZipArchive(zipStream, ZipArchiveMode.Read, false, Encoding.UTF8)) { if (logSettings && !isConfigTransfer) { ZipArchiveEntry entry = backupZip.GetEntry("log.config"); if (entry is not null) { //dynamically load and apply logger config await using (Stream stream = entry.Open()) { _log.LoadConfig(stream); } } } if (logs && !isConfigTransfer) { _log.BulkManipulateLogFiles(delegate () { if (deleteExistingFiles) { //delete existing log files string[] logFiles = Directory.GetFiles(_log.LogFolderAbsolutePath, "*.log", SearchOption.TopDirectoryOnly); foreach (string logFile in logFiles) { try { File.Delete(logFile); } catch (Exception ex) { _log.Write(ex); } } } //extract log files from backup foreach (ZipArchiveEntry entry in backupZip.Entries) { if (entry.FullName.StartsWith("logs/")) { try { entry.ExtractToFile(Path.Combine(_log.LogFolderAbsolutePath, entry.Name), true); } catch (Exception ex) { _log.Write(ex); } } } }); } if (authConfig) { ZipArchiveEntry entry = backupZip.GetEntry("auth.config"); if (entry is not null) { //dynamically load and apply auth config await using (Stream stream = entry.Open()) { _authManager.LoadConfig(stream, isConfigTransfer, implantSession); } } } if (clusterConfig && !isConfigTransfer) { ZipArchiveEntry entry = backupZip.GetEntry("cluster.config"); if (entry is not null) { //dynamically load and apply cluster config await using (Stream stream = entry.Open()) { _clusterManager.LoadConfig(stream); } } } if ((webServiceSettings || dnsSettings) && !isConfigTransfer) { //extract any certs foreach (ZipArchiveEntry certEntry in backupZip.Entries) { if (certEntry.FullName.StartsWith("apps/")) continue; if (certEntry.FullName.EndsWith(".pfx", StringComparison.OrdinalIgnoreCase) || certEntry.FullName.EndsWith(".p12", StringComparison.OrdinalIgnoreCase)) { string certFile = Path.Combine(_configFolder, certEntry.FullName); try { Directory.CreateDirectory(Path.GetDirectoryName(certFile)); certEntry.ExtractToFile(certFile, true); } catch (Exception ex) { _log.Write(ex); } } } } if (webServiceSettings && !isConfigTransfer) { ZipArchiveEntry entry = backupZip.GetEntry("webservice.config"); if (entry is not null) { //dynamically load and apply web service config await using (Stream stream = entry.Open()) { LoadConfig(stream); } } } if (dnsSettings) { ZipArchiveEntry entry = backupZip.GetEntry("dns.config"); if (entry is not null) { try { //dynamically load and apply DNS settings config await using (Stream stream = entry.Open()) { _dnsServer.LoadConfig(stream, isConfigTransfer); } } catch (InvalidDataException) { if (isConfigTransfer) throw; //config being synced; throw same exception //most probably an attempt to restore old config await using (Stream stream = entry.Open()) { if (!TryLoadOldConfigFrom(stream)) throw; //was not old config file so must be corrupt config file; throw same exception _log.Write("Old DNS config file was restored successfully."); //explicitly save webservice.config SaveConfigFileInternal(); } } } } if (zones) { if (isConfigTransfer) { //backup DNSSEC private keys into Secondary zones that are member zone of the secondary cluster catalog zone AuthZoneInfo secondaryClusterCatalogZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo("cluster-catalog." + _clusterManager.ClusterDomain); if ((secondaryClusterCatalogZoneInfo is not null) && (secondaryClusterCatalogZoneInfo.Type == AuthZoneType.SecondaryCatalog)) { HashSet memberZoneNames = new HashSet((secondaryClusterCatalogZoneInfo.ApexZone as SecondaryCatalogZone).GetAllMemberZoneNames()); foreach (ZipArchiveEntry entry in backupZip.Entries) { if (!entry.FullName.StartsWith("zones/") || !entry.FullName.EndsWith(".keys", StringComparison.Ordinal)) continue; string memberZoneName = Path.GetFileNameWithoutExtension(entry.Name); AuthZoneInfo memberZoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; //no such zone exists; ignore if (memberZoneInfo.Type != AuthZoneType.Secondary) continue; //not a Secondary zone; ignore SecondaryZone memberZone = memberZoneInfo.ApexZone as SecondaryZone; if (memberZoneNames.Contains(memberZoneName)) { //read DNSSEC private keys IReadOnlyCollection dnssecPrivateKeys; await using (Stream s = entry.Open()) { dnssecPrivateKeys = AuthZoneInfo.ReadDnssecPrivateKeysFrom(new BinaryReader(s)); } //backup DNSSEC private keys memberZone.DnssecPrivateKeys = dnssecPrivateKeys; _dnsServer.AuthZoneManager.SaveZoneFile(memberZoneInfo.Name); } else { //not a member zone of the secondary cluster catalog zone if (memberZone.DnssecPrivateKeys is not null) { //found old backup keys; remove them memberZone.DnssecPrivateKeys = null; _dnsServer.AuthZoneManager.SaveZoneFile(memberZoneInfo.Name); } } } } } else { //restore zones if (deleteExistingFiles) { //delete existing zone files string[] zoneFiles = Directory.GetFiles(Path.Combine(_configFolder, "zones"), "*.zone", SearchOption.TopDirectoryOnly); foreach (string zoneFile in zoneFiles) { try { File.Delete(zoneFile); } catch (Exception ex) { _log.Write(ex); } } } //extract zone files from backup foreach (ZipArchiveEntry entry in backupZip.Entries) { if (entry.FullName.StartsWith("zones/")) { try { entry.ExtractToFile(Path.Combine(_configFolder, "zones", entry.Name), true); } catch (Exception ex) { _log.Write(ex); } } } //reload zones _dnsServer.AuthZoneManager.LoadAllZoneFiles(); InspectAndFixZonePermissions(); } } if (allowedZones) { ZipArchiveEntry entry = backupZip.GetEntry("allowed.config"); if (entry is not null) { //dynamically load and apply allowed zones config await using (Stream stream = entry.Open()) { _dnsServer.AllowedZoneManager.LoadAllowedZone(stream); } } } if (blockedZones) { ZipArchiveEntry entry = backupZip.GetEntry("blocked.config"); if (entry is not null) { //dynamically load and apply blocked zones config await using (Stream stream = entry.Open()) { _dnsServer.BlockedZoneManager.LoadBlockedZone(stream); } } } if (blockLists) { if (deleteExistingFiles) { //delete existing block list files string[] blockListFiles = Directory.GetFiles(Path.Combine(_configFolder, "blocklists"), "*", SearchOption.TopDirectoryOnly); foreach (string blockListFile in blockListFiles) { try { File.Delete(blockListFile); } catch (Exception ex) { _log.Write(ex); } } } //extract block list files from backup foreach (ZipArchiveEntry entry in backupZip.Entries) { if (entry.FullName.StartsWith("blocklists/")) { try { entry.ExtractToFile(Path.Combine(_configFolder, "blocklists", entry.Name), true); } catch (IOException) { //ignore since file may be loading in another thread } catch (Exception ex) { _log.Write(ex); } } } ZipArchiveEntry blockListConfigEntry = backupZip.GetEntry("blocklist.config"); if (blockListConfigEntry is not null) { //dynamically load and apply block list config await using (Stream stream = blockListConfigEntry.Open()) { _dnsServer.BlockListZoneManager.LoadConfig(stream, isConfigTransfer); } } } if (apps) { if (isConfigTransfer) { //install or update app from zip foreach (ZipArchiveEntry entry in backupZip.Entries) { if (!entry.FullName.StartsWith("apps/")) continue; string[] fullNameParts = entry.FullName.Split('/'); if (fullNameParts.Length < 3) continue; string applicationName = fullNameParts[1]; string applicationZipFile = fullNameParts[2]; if (!applicationZipFile.Equals(applicationName + ".zip", StringComparison.Ordinal)) continue; if (_dnsServer.DnsApplicationManager.Applications.TryGetValue(applicationName, out _)) { //update existing app await using (Stream s = entry.Open()) { await _dnsServer.DnsApplicationManager.UpdateApplicationAsync(applicationName, s); } } else { //install new app await using (Stream s = entry.Open()) { await _dnsServer.DnsApplicationManager.InstallApplicationAsync(applicationName, s); } } } //update app config foreach (ZipArchiveEntry entry in backupZip.Entries) { if (!entry.FullName.StartsWith("apps/")) continue; string[] fullNameParts = entry.FullName.Split('/'); if (fullNameParts.Length < 3) continue; string applicationName = fullNameParts[1]; string configFile = fullNameParts[2]; if (!configFile.Equals("dnsApp.config", StringComparison.Ordinal)) continue; if (_dnsServer.DnsApplicationManager.Applications.TryGetValue(applicationName, out DnsApplication application)) { string config; await using (Stream s = entry.Open()) { using (StreamReader sR = new StreamReader(s, true)) { config = await sR.ReadToEndAsync(); } } try { await application.SetConfigAsync(config); } catch (Exception ex) { _log.Write(ex); } } } //remove apps that are not in the zip file HashSet existingApplications = new HashSet(); foreach (ZipArchiveEntry entry in backupZip.Entries) { if (!entry.FullName.StartsWith("apps/")) continue; string[] fullNameParts = entry.FullName.Split('/'); if (fullNameParts.Length < 2) continue; string applicationName = fullNameParts[1]; existingApplications.Add(applicationName); } foreach (KeyValuePair application in _dnsServer.DnsApplicationManager.Applications) { if (!existingApplications.Contains(application.Key)) _dnsServer.DnsApplicationManager.UninstallApplication(application.Key); } } else { //unload apps _dnsServer.DnsApplicationManager.UnloadAllApplications(); if (deleteExistingFiles) { //delete existing apps string appFolder = Path.Combine(_configFolder, "apps"); if (Directory.Exists(appFolder)) { try { Directory.Delete(appFolder, true); } catch (Exception ex) { _log.Write(ex); } } //create apps folder Directory.CreateDirectory(appFolder); } //extract apps files from backup foreach (ZipArchiveEntry entry in backupZip.Entries) { if (entry.FullName.StartsWith("apps/")) { string entryPath = entry.FullName; if (Path.DirectorySeparatorChar != '/') entryPath = entryPath.Replace('/', '\\'); string filePath = Path.Combine(_configFolder, entryPath); Directory.CreateDirectory(Path.GetDirectoryName(filePath)); try { entry.ExtractToFile(filePath, true); } catch (Exception ex) { _log.Write(ex); } } } //reload apps await _dnsServer.DnsApplicationManager.LoadAllApplicationsAsync(); } } if (scopes && !isConfigTransfer) { //stop dhcp server _dhcpServer.Stop(); try { if (deleteExistingFiles) { //delete existing scope files string[] scopeFiles = Directory.GetFiles(Path.Combine(_configFolder, "scopes"), "*.scope", SearchOption.TopDirectoryOnly); foreach (string scopeFile in scopeFiles) { try { File.Delete(scopeFile); } catch (Exception ex) { _log.Write(ex); } } } //extract scope files from backup foreach (ZipArchiveEntry entry in backupZip.Entries) { if (entry.FullName.StartsWith("scopes/")) { try { entry.ExtractToFile(Path.Combine(_configFolder, "scopes", entry.Name), true); } catch (Exception ex) { _log.Write(ex); } } } } finally { //start dhcp server _dhcpServer.Start(); } } if (stats && !isConfigTransfer) { if (deleteExistingFiles) { //delete existing stats files string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly); foreach (string hourlyStatsFile in hourlyStatsFiles) { try { File.Delete(hourlyStatsFile); } catch (Exception ex) { _log.Write(ex); } } string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_configFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly); foreach (string dailyStatsFile in dailyStatsFiles) { try { File.Delete(dailyStatsFile); } catch (Exception ex) { _log.Write(ex); } } } //extract stats files from backup foreach (ZipArchiveEntry entry in backupZip.Entries) { if (entry.FullName.StartsWith("stats/")) { try { entry.ExtractToFile(Path.Combine(_configFolder, "stats", entry.Name), true); } catch (Exception ex) { _log.Write(ex); } } } //reload stats _dnsServer.StatsManager.ReloadStats(); } } } private static async Task CreateBackupEntryFromSharedFileAsync(ZipArchive backupZip, string sourceFileName, string entryName) { await using (FileStream fS = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { ZipArchiveEntry entry = backupZip.CreateEntry(entryName); DateTime lastWrite = File.GetLastWriteTime(sourceFileName); // If file to be archived has an invalid last modified time, use the first datetime representable in the Zip timestamp format // (midnight on January 1, 1980): if (lastWrite.Year < 1980 || lastWrite.Year > 2107) lastWrite = new DateTime(1980, 1, 1, 0, 0, 0); entry.LastWriteTime = lastWrite; await using (Stream sE = entry.Open()) { await fS.CopyToAsync(sE); } } } #endregion #region internal private string ConvertToRelativePath(string path) { if (path.StartsWith(_configFolder, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar); return path; } private string ConvertToAbsolutePath(string path) { if (path is null) return null; if (Path.IsPathRooted(path)) return path; return Path.Combine(_configFolder, path); } #endregion #region server version private string GetServerVersion() { return GetCleanVersion(_currentVersion); } private static string GetCleanVersion(Version version) { string strVersion = version.Major + "." + version.Minor; if (version.Build > 0) strVersion += "." + version.Build; if (version.Revision > 0) strVersion += "." + version.Revision; return strVersion; } #endregion #region web service private async Task TryStartWebServiceAsync(IReadOnlyList oldWebServiceLocalAddresses, int oldWebServiceHttpPort, int oldWebServiceTlsPort) { try { _webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(_webServiceLocalAddresses); await StartWebServiceAsync(false); return; } catch (Exception ex) { _log.Write("Web Service failed to start: " + ex.ToString()); } _log.Write("Attempting to revert Web Service end point changes ..."); try { _webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(oldWebServiceLocalAddresses); _webServiceHttpPort = oldWebServiceHttpPort; _webServiceTlsPort = oldWebServiceTlsPort; await StartWebServiceAsync(false); SaveConfigFileInternal(); //save reverted changes return; } catch (Exception ex2) { _log.Write("Web Service failed to start: " + ex2.ToString()); } _log.Write("Attempting to start Web Service on ANY (0.0.0.0) fallback address..."); try { _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any }; await StartWebServiceAsync(true); return; } catch (Exception ex3) { _log.Write("Web Service failed to start: " + ex3.ToString()); } _log.Write("Attempting to start Web Service on loopback (127.0.0.1) fallback address..."); _webServiceLocalAddresses = new IPAddress[] { IPAddress.Loopback }; await StartWebServiceAsync(true); } private async Task StartWebServiceAsync(bool httpOnlyMode) { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_appFolder) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.Environment.WebRootFileProvider = new PhysicalFileProvider(Path.Combine(_appFolder, "www")) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.Services.AddResponseCompression(delegate (ResponseCompressionOptions options) { options.EnableForHttps = true; }); builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions) { //http foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) serverOptions.Listen(webServiceLocalAddress, _webServiceHttpPort); //https if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null)) { foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) { serverOptions.Listen(webServiceLocalAddress, _webServiceTlsPort, delegate (ListenOptions listenOptions) { if (_webServiceEnableHttp3) listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; else if (IsHttp2Supported()) listenOptions.Protocols = HttpProtocols.Http1AndHttp2; else listenOptions.Protocols = HttpProtocols.Http1; listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { return ValueTask.FromResult(_webServiceSslServerAuthenticationOptions); }, null); }); } } serverOptions.AddServerHeader = false; serverOptions.Limits.MaxRequestBodySize = int.MaxValue; }); builder.Services.Configure(delegate (FormOptions options) { options.MultipartBodyLengthLimit = int.MaxValue; }); builder.Logging.ClearProviders(); _webService = builder.Build(); _webService.UseResponseCompression(); if (_webServiceHttpToTlsRedirect && !httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null)) _webService.Use(WebServiceHttpsRedirectionMiddleware); _webService.UseDefaultFiles(); _webService.UseStaticFiles(new StaticFileOptions() { OnPrepareResponse = delegate (StaticFileResponseContext ctx) { ctx.Context.Response.Headers["X-Robots-Tag"] = "noindex, nofollow"; ctx.Context.Response.Headers.CacheControl = "no-cache"; }, ServeUnknownFileTypes = true }); ConfigureWebServiceRoutes(); try { await _webService.StartAsync(); foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) { _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Http", "Web Service was bound successfully."); if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null)) _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort), "Https", "Web Service was bound successfully."); } } catch { await StopWebServiceAsync(); foreach (IPAddress webServiceLocalAddress in _webServiceLocalAddresses) { _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceHttpPort), "Http", "Web Service failed to bind."); if (!httpOnlyMode && _webServiceEnableTls && (_webServiceSslServerAuthenticationOptions is not null)) _log.Write(new IPEndPoint(webServiceLocalAddress, _webServiceTlsPort), "Https", "Web Service failed to bind."); } throw; } } private async Task StopWebServiceAsync() { if (_webService is not null) { await _webService.DisposeAsync(); _webService = null; } } private bool IsHttp2Supported() { if (_webServiceEnableHttp3) return true; switch (Environment.OSVersion.Platform) { case PlatformID.Win32NT: return Environment.OSVersion.Version.Major >= 10; //http/2 supported on Windows Server 2016/Windows 10 or later case PlatformID.Unix: return true; //http/2 supported on Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later) default: return false; } } private void ConfigureWebServiceRoutes() { _webService.UseExceptionHandler(WebServiceExceptionHandler); _webService.Use(WebServiceApiMiddleware); _webService.UseRouting(); //user auth _webService.MapGetAndPost("/api/user/login", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.Standard); }); _webService.MapGetAndPost("/api/user/createToken", delegate (HttpContext context) { return _authApi.LoginAsync(context, UserSessionType.ApiToken); }); _webService.MapGetAndPost("/api/user/logout", _authApi.Logout); //user _webService.MapGetAndPost("/api/user/session/get", _authApi.GetCurrentSessionDetails); _webService.MapGetAndPost("/api/user/session/delete", delegate (HttpContext context) { _authApi.DeleteSession(context, false); }); _webService.MapGetAndPost("/api/user/changePassword", _authApi.ChangePasswordAsync); _webService.MapGetAndPost("/api/user/2fa/init", _authApi.Initialize2FA); _webService.MapGetAndPost("/api/user/2fa/enable", _authApi.Enable2FA); _webService.MapGetAndPost("/api/user/2fa/disable", _authApi.Disable2FA); _webService.MapGetAndPost("/api/user/profile/get", _authApi.GetProfile); _webService.MapGetAndPost("/api/user/profile/set", _authApi.SetProfile); _webService.MapGetAndPost("/api/user/checkForUpdate", _api.CheckForUpdateAsync); //dashboard _webService.MapGetAndPost("/api/dashboard/stats/get", _dashboardApi.GetStats); _webService.MapGetAndPost("/api/dashboard/stats/getTop", _dashboardApi.GetTopStats); _webService.MapGetAndPost("/api/dashboard/stats/deleteAll", _logsApi.DeleteAllStats); //zones _webService.MapGetAndPost("/api/zones/list", _zonesApi.ListZones); _webService.MapGetAndPost("/api/zones/catalogs/list", _zonesApi.ListCatalogZones); _webService.MapGetAndPost("/api/zones/create", _zonesApi.CreateZoneAsync); _webService.MapGetAndPost("/api/zones/import", _zonesApi.ImportZoneAsync); _webService.MapGetAndPost("/api/zones/export", _zonesApi.ExportZoneAsync); _webService.MapGetAndPost("/api/zones/clone", _zonesApi.CloneZone); _webService.MapGetAndPost("/api/zones/convert", _zonesApi.ConvertZone); _webService.MapGetAndPost("/api/zones/enable", _zonesApi.EnableZone); _webService.MapGetAndPost("/api/zones/disable", _zonesApi.DisableZone); _webService.MapGetAndPost("/api/zones/delete", _zonesApi.DeleteZone); _webService.MapGetAndPost("/api/zones/resync", _zonesApi.ResyncZone); _webService.MapGetAndPost("/api/zones/options/get", _zonesApi.GetZoneOptions); _webService.MapGetAndPost("/api/zones/options/set", _zonesApi.SetZoneOptions); _webService.MapGetAndPost("/api/zones/permissions/get", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Zones); }); _webService.MapGetAndPost("/api/zones/permissions/set", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Zones); }); _webService.MapGetAndPost("/api/zones/dnssec/sign", _zonesApi.SignPrimaryZone); _webService.MapGetAndPost("/api/zones/dnssec/unsign", _zonesApi.UnsignPrimaryZone); _webService.MapGetAndPost("/api/zones/dnssec/viewDS", _zonesApi.GetPrimaryZoneDsInfo); _webService.MapGetAndPost("/api/zones/dnssec/properties/get", _zonesApi.GetPrimaryZoneDnssecProperties); _webService.MapGetAndPost("/api/zones/dnssec/properties/convertToNSEC", _zonesApi.ConvertPrimaryZoneToNSEC); _webService.MapGetAndPost("/api/zones/dnssec/properties/convertToNSEC3", _zonesApi.ConvertPrimaryZoneToNSEC3); _webService.MapGetAndPost("/api/zones/dnssec/properties/updateNSEC3Params", _zonesApi.UpdatePrimaryZoneNSEC3Parameters); _webService.MapGetAndPost("/api/zones/dnssec/properties/updateDnsKeyTtl", _zonesApi.UpdatePrimaryZoneDnssecDnsKeyTtl); _webService.MapGetAndPost("/api/zones/dnssec/properties/generatePrivateKey", _zonesApi.AddPrimaryZoneDnssecPrivateKey); _webService.MapGetAndPost("/api/zones/dnssec/properties/addPrivateKey", _zonesApi.AddPrimaryZoneDnssecPrivateKey); _webService.MapGetAndPost("/api/zones/dnssec/properties/updatePrivateKey", _zonesApi.UpdatePrimaryZoneDnssecPrivateKey); _webService.MapGetAndPost("/api/zones/dnssec/properties/deletePrivateKey", _zonesApi.DeletePrimaryZoneDnssecPrivateKey); _webService.MapGetAndPost("/api/zones/dnssec/properties/publishAllPrivateKeys", _zonesApi.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys); _webService.MapGetAndPost("/api/zones/dnssec/properties/rolloverDnsKey", _zonesApi.RolloverPrimaryZoneDnsKey); _webService.MapGetAndPost("/api/zones/dnssec/properties/retireDnsKey", _zonesApi.RetirePrimaryZoneDnsKeyAsync); _webService.MapGetAndPost("/api/zones/records/add", _zonesApi.AddRecord); _webService.MapGetAndPost("/api/zones/records/get", _zonesApi.GetRecords); _webService.MapGetAndPost("/api/zones/records/update", _zonesApi.UpdateRecord); _webService.MapGetAndPost("/api/zones/records/delete", _zonesApi.DeleteRecord); //cache _webService.MapGetAndPost("/api/cache/list", _otherZonesApi.ListCachedZones); _webService.MapGetAndPost("/api/cache/delete", _otherZonesApi.DeleteCachedZone); _webService.MapGetAndPost("/api/cache/flush", _otherZonesApi.FlushCache); //allowed _webService.MapGetAndPost("/api/allowed/list", _otherZonesApi.ListAllowedZones); _webService.MapGetAndPost("/api/allowed/add", _otherZonesApi.AllowZone); _webService.MapGetAndPost("/api/allowed/delete", _otherZonesApi.DeleteAllowedZone); _webService.MapGetAndPost("/api/allowed/flush", _otherZonesApi.FlushAllowedZone); _webService.MapGetAndPost("/api/allowed/import", _otherZonesApi.ImportAllowedZones); _webService.MapGetAndPost("/api/allowed/export", _otherZonesApi.ExportAllowedZonesAsync); //blocked _webService.MapGetAndPost("/api/blocked/list", _otherZonesApi.ListBlockedZones); _webService.MapGetAndPost("/api/blocked/add", _otherZonesApi.BlockZone); _webService.MapGetAndPost("/api/blocked/delete", _otherZonesApi.DeleteBlockedZone); _webService.MapGetAndPost("/api/blocked/flush", _otherZonesApi.FlushBlockedZone); _webService.MapGetAndPost("/api/blocked/import", _otherZonesApi.ImportBlockedZones); _webService.MapGetAndPost("/api/blocked/export", _otherZonesApi.ExportBlockedZonesAsync); //apps _webService.MapGetAndPost("/api/apps/list", _appsApi.ListInstalledAppsAsync); _webService.MapGetAndPost("/api/apps/listStoreApps", _appsApi.ListStoreApps); _webService.MapGetAndPost("/api/apps/downloadAndInstall", _appsApi.DownloadAndInstallAppAsync); _webService.MapGetAndPost("/api/apps/downloadAndUpdate", _appsApi.DownloadAndUpdateAppAsync); _webService.MapPost("/api/apps/install", _appsApi.InstallAppAsync); _webService.MapPost("/api/apps/update", _appsApi.UpdateAppAsync); _webService.MapGetAndPost("/api/apps/uninstall", _appsApi.UninstallApp); _webService.MapGetAndPost("/api/apps/config/get", _appsApi.GetAppConfigAsync); _webService.MapGetAndPost("/api/apps/config/set", _appsApi.SetAppConfigAsync); //dns client _webService.MapGetAndPost("/api/dnsClient/resolve", _api.ResolveQueryAsync); //settings _webService.MapGetAndPost("/api/settings/get", _settingsApi.GetDnsSettings); _webService.MapGetAndPost("/api/settings/set", _settingsApi.SetDnsSettingsAsync); _webService.MapGetAndPost("/api/settings/getTsigKeyNames", _settingsApi.GetTsigKeyNames); _webService.MapGetAndPost("/api/settings/forceUpdateBlockLists", _settingsApi.ForceUpdateBlockLists); _webService.MapGetAndPost("/api/settings/temporaryDisableBlocking", _settingsApi.TemporaryDisableBlocking); _webService.MapGetAndPost("/api/settings/backup", _settingsApi.BackupSettingsAsync); _webService.MapPost("/api/settings/restore", _settingsApi.RestoreSettingsAsync); //dhcp _webService.MapGetAndPost("/api/dhcp/leases/list", _dhcpApi.ListDhcpLeases); _webService.MapGetAndPost("/api/dhcp/leases/remove", _dhcpApi.RemoveDhcpLease); _webService.MapGetAndPost("/api/dhcp/leases/convertToReserved", _dhcpApi.ConvertToReservedLease); _webService.MapGetAndPost("/api/dhcp/leases/convertToDynamic", _dhcpApi.ConvertToDynamicLease); _webService.MapGetAndPost("/api/dhcp/scopes/list", _dhcpApi.ListDhcpScopes); _webService.MapGetAndPost("/api/dhcp/scopes/get", _dhcpApi.GetDhcpScope); _webService.MapGetAndPost("/api/dhcp/scopes/set", _dhcpApi.SetDhcpScopeAsync); _webService.MapGetAndPost("/api/dhcp/scopes/addReservedLease", _dhcpApi.AddReservedLease); _webService.MapGetAndPost("/api/dhcp/scopes/removeReservedLease", _dhcpApi.RemoveReservedLease); _webService.MapGetAndPost("/api/dhcp/scopes/enable", _dhcpApi.EnableDhcpScopeAsync); _webService.MapGetAndPost("/api/dhcp/scopes/disable", _dhcpApi.DisableDhcpScope); _webService.MapGetAndPost("/api/dhcp/scopes/delete", _dhcpApi.DeleteDhcpScope); //administration _webService.MapGetAndPost("/api/admin/sessions/list", _authApi.ListSessions); _webService.MapGetAndPost("/api/admin/sessions/createToken", _authApi.CreateApiToken); _webService.MapGetAndPost("/api/admin/sessions/delete", delegate (HttpContext context) { _authApi.DeleteSession(context, true); }); _webService.MapGetAndPost("/api/admin/users/list", _authApi.ListUsers); _webService.MapGetAndPost("/api/admin/users/create", _authApi.CreateUser); _webService.MapGetAndPost("/api/admin/users/get", _authApi.GetUserDetails); _webService.MapGetAndPost("/api/admin/users/set", _authApi.SetUserDetails); _webService.MapGetAndPost("/api/admin/users/delete", _authApi.DeleteUser); _webService.MapGetAndPost("/api/admin/groups/list", _authApi.ListGroups); _webService.MapGetAndPost("/api/admin/groups/create", _authApi.CreateGroup); _webService.MapGetAndPost("/api/admin/groups/get", _authApi.GetGroupDetails); _webService.MapGetAndPost("/api/admin/groups/set", _authApi.SetGroupDetails); _webService.MapGetAndPost("/api/admin/groups/delete", _authApi.DeleteGroup); _webService.MapGetAndPost("/api/admin/permissions/list", _authApi.ListPermissions); _webService.MapGetAndPost("/api/admin/permissions/get", delegate (HttpContext context) { _authApi.GetPermissionDetails(context, PermissionSection.Unknown); }); _webService.MapGetAndPost("/api/admin/permissions/set", delegate (HttpContext context) { _authApi.SetPermissionsDetails(context, PermissionSection.Unknown); }); _webService.MapGetAndPost("/api/admin/cluster/state", _clusterApi.GetClusterState); _webService.MapGetAndPost("/api/admin/cluster/init", _clusterApi.InitializeCluster); _webService.MapGetAndPost("/api/admin/cluster/primary/delete", _clusterApi.DeleteCluster); _webService.MapGetAndPost("/api/admin/cluster/primary/join", _clusterApi.JoinCluster); _webService.MapGetAndPost("/api/admin/cluster/primary/removeSecondary", _clusterApi.RemoveSecondaryNodeAsync); _webService.MapGetAndPost("/api/admin/cluster/primary/deleteSecondary", _clusterApi.DeleteSecondaryNode); _webService.MapGetAndPost("/api/admin/cluster/primary/updateSecondary", _clusterApi.UpdateSecondaryNode); _webService.MapGetAndPost("/api/admin/cluster/primary/transferConfig", _clusterApi.TransferConfigAsync); _webService.MapGetAndPost("/api/admin/cluster/primary/setOptions", _clusterApi.SetClusterOptions); _webService.MapPost("/api/admin/cluster/initJoin", _clusterApi.InitializeAndJoinClusterAsync); _webService.MapGetAndPost("/api/admin/cluster/secondary/leave", _clusterApi.LeaveClusterAsync); _webService.MapGetAndPost("/api/admin/cluster/secondary/notify", _clusterApi.ConfigUpdateNotificationAsync); _webService.MapGetAndPost("/api/admin/cluster/secondary/resync", _clusterApi.ResyncCluster); _webService.MapGetAndPost("/api/admin/cluster/secondary/updatePrimary", _clusterApi.UpdatePrimaryNodeAsync); _webService.MapGetAndPost("/api/admin/cluster/secondary/promote", _clusterApi.PromoteToPrimaryNodeAsync); _webService.MapGetAndPost("/api/admin/cluster/updateIpAddress", _clusterApi.UpdateSelfNodeIPAddress); //logs _webService.MapGetAndPost("/api/logs/list", _logsApi.ListLogs); _webService.MapGetAndPost("/api/logs/download", _logsApi.DownloadLogAsync); _webService.MapGetAndPost("/api/logs/delete", _logsApi.DeleteLog); _webService.MapGetAndPost("/api/logs/deleteAll", _logsApi.DeleteAllLogs); _webService.MapGetAndPost("/api/logs/query", _logsApi.QueryLogsAsync); _webService.MapGetAndPost("/api/logs/export", _logsApi.ExportLogsAsync); //fallback _webService.MapFallback("/api/{*path}", delegate (HttpContext context) { //mark api fallback context.Items["apiFallback"] = string.Empty; }); } private static ClusterNodeType GetClusterNodeTypeForPath(string path) { switch (path) { case "/api/user/createToken": case "/api/user/changePassword": case "/api/user/2fa/init": case "/api/user/2fa/enable": case "/api/user/2fa/disable": case "/api/user/profile/set": case "/api/allowed/add": case "/api/allowed/delete": case "/api/allowed/flush": case "/api/allowed/import": case "/api/blocked/add": case "/api/blocked/delete": case "/api/blocked/flush": case "/api/blocked/import": case "/api/apps/downloadAndInstall": case "/api/apps/downloadAndUpdate": case "/api/apps/install": case "/api/apps/update": case "/api/apps/uninstall": case "/api/apps/config/set": case "/api/admin/sessions/createToken": case "/api/admin/users/create": case "/api/admin/users/set": case "/api/admin/users/delete": case "/api/admin/groups/create": case "/api/admin/groups/set": case "/api/admin/groups/delete": return ClusterNodeType.Primary; //this api can be called only on primary node case "/api/user/login": case "/api/user/logout": case "/api/user/session/get": case "/api/user/session/delete": return ClusterNodeType.Secondary; //this api must be called on current node default: return ClusterNodeType.Unknown; //this api can be called on any specified node } } private Task WebServiceHttpsRedirectionMiddleware(HttpContext context, RequestDelegate next) { if (context.Request.IsHttps) return next(context); context.Response.Redirect("https://" + (context.Request.Host.HasValue ? context.Request.Host.Host : _dnsServer.ServerDomain) + (_webServiceTlsPort == 443 ? "" : ":" + _webServiceTlsPort) + context.Request.Path + (context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""), false, true); return Task.CompletedTask; } private async Task WebServiceApiMiddleware(HttpContext context, RequestDelegate next) { HttpRequest request = context.Request; if (_clusterManager.ClusterInitialized) { ClusterNodeType pathNodeType = GetClusterNodeTypeForPath(request.Path); switch (pathNodeType) { case ClusterNodeType.Primary: //this api can be called only on primary node ClusterNode selfNode = _clusterManager.GetSelfNode(); if (selfNode.Type == ClusterNodeType.Secondary) { //validate user session before proxying request if (!TryGetSession(context, out UserSession session)) throw new InvalidTokenWebServiceException("Invalid token or session expired."); //proxy to primary node ClusterNode primaryNode = _clusterManager.GetPrimaryNode(); await primaryNode.ProxyRequest(context, session.User.Username); return; } break; case ClusterNodeType.Secondary: //this api must be called on current node break; default: //this api can be called on any specified node string nodeName = request.GetQueryOrForm("node", null); if (!string.IsNullOrEmpty(nodeName) && (nodeName != "cluster")) { if (!_clusterManager.TryGetClusterNode(nodeName, out ClusterNode node)) throw new DnsWebServiceException("No such node exists in the Cluster by name: " + nodeName); if (node.State != ClusterNodeState.Self) { //validate user session before proxying request if (!TryGetSession(context, out UserSession session)) throw new InvalidTokenWebServiceException("Invalid token or session expired."); //proxy request to the specified cluster node await node.ProxyRequest(context, session.User.Username); return; } } break; } } bool needsJsonResponseObject; switch (request.Path) { case "/api/user/login": case "/api/user/createToken": case "/api/user/logout": needsJsonResponseObject = false; break; case "/api/user/session/get": { if (!TryGetSession(context, out UserSession session)) throw new InvalidTokenWebServiceException("Invalid token or session expired."); context.Items["session"] = session; needsJsonResponseObject = false; } break; case "/api/zones/export": case "/api/allowed/export": case "/api/blocked/export": case "/api/settings/backup": case "/api/logs/download": case "/api/logs/export": case "/api/admin/cluster/primary/transferConfig": { if (!TryGetSession(context, out UserSession session)) throw new InvalidTokenWebServiceException("Invalid token or session expired."); context.Items["session"] = session; await next(context); } return; default: if (request.Path.Value.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { if (!TryGetSession(context, out UserSession session)) throw new InvalidTokenWebServiceException("Invalid token or session expired."); context.Items["session"] = session; needsJsonResponseObject = true; } else { HttpResponse response = context.Response; response.StatusCode = StatusCodes.Status404NotFound; response.ContentLength = 0; response.Headers.CacheControl = "no-cache, no-store, must-revalidate"; response.Headers.Pragma = "no-cache"; response.Headers.Expires = "0"; return; } break; } using (MemoryStream mS = new MemoryStream(4096)) { Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); context.Items["jsonWriter"] = jsonWriter; jsonWriter.WriteStartObject(); if (needsJsonResponseObject) { jsonWriter.WritePropertyName("response"); jsonWriter.WriteStartObject(); await next(context); jsonWriter.WriteEndObject(); } else { await next(context); } jsonWriter.WriteString("server", _dnsServer.ServerDomain); jsonWriter.WriteString("status", "ok"); jsonWriter.WriteEndObject(); jsonWriter.Flush(); mS.Position = 0; HttpResponse response = context.Response; response.Headers.CacheControl = "no-cache, no-store, must-revalidate"; response.Headers.Pragma = "no-cache"; response.Headers.Expires = "0"; object apiFallback = context.Items["apiFallback"]; //check api fallback mark if (apiFallback is null) { response.StatusCode = StatusCodes.Status200OK; response.ContentType = "application/json; charset=utf-8"; response.ContentLength = mS.Length; await mS.CopyToAsync(response.Body); } else { response.StatusCode = StatusCodes.Status404NotFound; response.ContentLength = 0; } } } private void WebServiceExceptionHandler(IApplicationBuilder exceptionHandlerApp) { exceptionHandlerApp.Run(async delegate (HttpContext context) { IExceptionHandlerPathFeature exceptionHandlerPathFeature = context.Features.Get(); if (exceptionHandlerPathFeature.Path.StartsWith("/api/")) { Exception ex = exceptionHandlerPathFeature.Error; HttpResponse response = context.Response; response.StatusCode = StatusCodes.Status200OK; response.Headers.CacheControl = "no-cache, no-store, must-revalidate"; response.Headers.Pragma = "no-cache"; response.Headers.Expires = "0"; response.ContentType = "application/json; charset=utf-8"; await using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(response.Body)) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("server", _dnsServer.ServerDomain); if (ex is TwoFactorAuthRequiredWebServiceException) { jsonWriter.WriteString("status", "2fa-required"); jsonWriter.WriteString("errorMessage", ex.Message); } else if (ex is InvalidTokenWebServiceException) { jsonWriter.WriteString("status", "invalid-token"); jsonWriter.WriteString("errorMessage", ex.Message); } else { _log.Write(context.GetRemoteEndPoint(_webServiceRealIpHeader), ex); jsonWriter.WriteString("status", "error"); jsonWriter.WriteString("errorMessage", ex.Message); jsonWriter.WriteString("stackTrace", ex.StackTrace); if (ex.InnerException is not null) jsonWriter.WriteString("innerErrorMessage", ex.InnerException.Message); } jsonWriter.WriteEndObject(); } } }); } private bool TryGetSession(HttpContext context, out UserSession session) { string token = context.Request.GetQueryOrForm("token"); session = _authManager.GetSession(token); if ((session is null) || session.User.Disabled) return false; if (session.HasExpired()) { _authManager.DeleteSession(session.Token); _authManager.SaveConfigFile(); return false; } IPEndPoint remoteEP = context.GetRemoteEndPoint(_webServiceRealIpHeader); session.UpdateLastSeen(remoteEP.Address, context.Request.Headers.UserAgent); return true; } private User GetSessionUser(HttpContext context, bool standardOnly = false) { UserSession session = context.GetCurrentSession(); if ((session.Type == UserSessionType.ApiToken) && _clusterManager.ClusterInitialized && session.TokenName.Equals(_clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase)) { //proxy call from cluster node string username = context.Request.GetQueryOrForm("actingUser", null); if (username is null) return session.User; User user = _authManager.GetUser(username); if (user is null) throw new DnsWebServiceException("No such user exists: " + username); return user; } else { if (standardOnly && (session.Type != UserSessionType.Standard)) throw new DnsWebServiceException("Access was denied."); return session.User; } } #endregion #region tls private void StartTlsCertificateUpdateTimer() { if (_tlsCertificateUpdateTimer is null) { _tlsCertificateUpdateTimer = new Timer(delegate (object state) { if (!string.IsNullOrEmpty(_webServiceTlsCertificatePath)) { string webServiceTlsCertificatePath = ConvertToAbsolutePath(_webServiceTlsCertificatePath); try { FileInfo fileInfo = new FileInfo(webServiceTlsCertificatePath); if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServiceCertificateLastModifiedOn)) { LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, _webServiceTlsCertificatePassword); if (_clusterManager.ClusterInitialized) _clusterManager.UpdateSelfNodeUrlAndCertificate(); } } catch (Exception ex) { _log.Write("DNS Server encountered an error while updating Web Service TLS Certificate: " + webServiceTlsCertificatePath + "\r\n" + ex.ToString()); } } }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL); } } private void StopTlsCertificateUpdateTimer() { if (_tlsCertificateUpdateTimer is not null) { _tlsCertificateUpdateTimer.Dispose(); _tlsCertificateUpdateTimer = null; } } private void LoadWebServiceTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword) { FileInfo fileInfo = new FileInfo(tlsCertificatePath); if (!fileInfo.Exists) throw new ArgumentException("Web Service TLS certificate file does not exists: " + tlsCertificatePath); switch (Path.GetExtension(tlsCertificatePath).ToLowerInvariant()) { case ".pfx": case ".p12": break; default: throw new ArgumentException("Web Service TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: " + tlsCertificatePath); } X509Certificate2Collection certificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(tlsCertificatePath, tlsCertificatePassword, X509KeyStorageFlags.PersistKeySet); X509Certificate2 serverCertificate = null; foreach (X509Certificate2 certificate in certificateCollection) { if (certificate.HasPrivateKey) { serverCertificate = certificate; break; } } if (serverCertificate is null) throw new ArgumentException("Web Service TLS certificate file must contain a certificate with private key."); List applicationProtocols = new List(); if (_webServiceEnableHttp3) applicationProtocols.Add(new SslApplicationProtocol("h3")); if (IsHttp2Supported()) applicationProtocols.Add(new SslApplicationProtocol("h2")); applicationProtocols.Add(new SslApplicationProtocol("http/1.1")); _webServiceSslServerAuthenticationOptions = new SslServerAuthenticationOptions { ApplicationProtocols = applicationProtocols, ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, certificateCollection, false) }; _webServiceCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc; _log.Write("Web Service TLS certificate was loaded: " + tlsCertificatePath); } private void RemoveWebServiceTlsCertificate() { _webServiceSslServerAuthenticationOptions = null; _webServiceTlsCertificatePath = null; _webServiceTlsCertificatePassword = null; StopTlsCertificateUpdateTimer(); } public void SetWebServiceTlsCertificate(string webServiceTlsCertificatePath, string webServiceTlsCertificatePassword) { if (string.IsNullOrWhiteSpace(webServiceTlsCertificatePath)) throw new ArgumentException("Web service TLS certificate path cannot be null or empty.", nameof(webServiceTlsCertificatePath)); if (webServiceTlsCertificatePath.Length > 255) throw new ArgumentException("Web service TLS certificate path length cannot exceed 255 characters.", nameof(webServiceTlsCertificatePath)); if (webServiceTlsCertificatePassword?.Length > 255) throw new ArgumentException("Web service TLS certificate password length cannot exceed 255 characters.", nameof(webServiceTlsCertificatePassword)); webServiceTlsCertificatePath = ConvertToAbsolutePath(webServiceTlsCertificatePath); try { LoadWebServiceTlsCertificate(webServiceTlsCertificatePath, webServiceTlsCertificatePassword); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading Web Service TLS Certificate: " + webServiceTlsCertificatePath + "\r\n" + ex.ToString()); } _webServiceTlsCertificatePath = ConvertToRelativePath(webServiceTlsCertificatePath); _webServiceTlsCertificatePassword = webServiceTlsCertificatePassword; StartTlsCertificateUpdateTimer(); } private void CheckAndLoadSelfSignedCertificate(bool forceGenerateNew, bool throwException) { string selfSignedCertificateFilePath = Path.Combine(_configFolder, "self-signed-cert.pfx"); if (_webServiceUseSelfSignedTlsCertificate) { string oldSelfSignedCertificateFilePath = Path.Combine(_configFolder, "cert.pfx"); if (!oldSelfSignedCertificateFilePath.Equals(ConvertToAbsolutePath(_webServiceTlsCertificatePath), Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) && File.Exists(oldSelfSignedCertificateFilePath) && !File.Exists(selfSignedCertificateFilePath)) File.Move(oldSelfSignedCertificateFilePath, selfSignedCertificateFilePath); if (forceGenerateNew || !File.Exists(selfSignedCertificateFilePath)) { RSA rsa = RSA.Create(2048); CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); SubjectAlternativeNameBuilder san = new SubjectAlternativeNameBuilder(); bool sanAdded = false; foreach (IPAddress localAddress in _webServiceLocalAddresses) { if (localAddress.Equals(IPAddress.IPv6Any) || localAddress.Equals(IPAddress.Any)) continue; san.AddIpAddress(localAddress); sanAdded = true; } if (sanAdded) req.CertificateExtensions.Add(san.Build()); X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5)); File.WriteAllBytes(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string)); } if (_webServiceEnableTls && string.IsNullOrEmpty(_webServiceTlsCertificatePath)) { try { LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null); if (!forceGenerateNew) { if (_webServiceSslServerAuthenticationOptions.ServerCertificateContext.TargetCertificate.NotAfter < DateTime.UtcNow.AddYears(1)) { _log.Write("Web Service TLS self signed certificate is nearing expiration and will be regenerated."); CheckAndLoadSelfSignedCertificate(true, throwException); //force generate new cert if (_clusterManager.ClusterInitialized) _clusterManager.UpdateSelfNodeUrlAndCertificate(); } } } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading self signed Web Service TLS certificate: " + selfSignedCertificateFilePath + "\r\n" + ex.ToString()); if (throwException) throw; } } } else { File.Delete(selfSignedCertificateFilePath); } } #endregion #region quic private static void ValidateQuicSupport(string protocolName = "DNS-over-QUIC") { #pragma warning disable CA2252 // This API requires opting into preview features #pragma warning disable CA1416 // Validate platform compatibility if (!QuicConnection.IsSupported) throw new DnsWebServiceException(protocolName + " is supported only on Windows 11, Windows Server 2022, and Linux. On Linux, you must install 'libmsquic' manually."); #pragma warning restore CA1416 // Validate platform compatibility #pragma warning restore CA2252 // This API requires opting into preview features } private static bool IsQuicSupported() { #pragma warning disable CA2252 // This API requires opting into preview features #pragma warning disable CA1416 // Validate platform compatibility return QuicConnection.IsSupported; #pragma warning restore CA1416 // Validate platform compatibility #pragma warning restore CA2252 // This API requires opting into preview features } #endregion #region secondary catalog zones private void AuthZoneManager_SecondaryCatalogZoneAdded(object sender, SecondaryCatalogEventArgs e) { AuthZoneInfo secondaryCatalogZoneInfo = new AuthZoneInfo(sender as ApexZone); AuthZoneInfo memberZoneInfo = e.ZoneInfo; //clone user/group permissions from source zone Permission sourceZonePermissions = _authManager.GetPermission(PermissionSection.Zones, secondaryCatalogZoneInfo.Name); foreach (KeyValuePair userPermission in sourceZonePermissions.UserPermissions) _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, userPermission.Key, userPermission.Value); foreach (KeyValuePair groupPermissions in sourceZonePermissions.GroupPermissions) _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, groupPermissions.Key, groupPermissions.Value); //set default permissions _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, memberZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); //sync dnssec private keys for secondary members zone when it is a cluster secondary catalog zone if (_clusterManager.ClusterInitialized && (memberZoneInfo.Type == AuthZoneType.Secondary) && secondaryCatalogZoneInfo.Name.Equals("cluster-catalog." + _clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase)) _clusterManager.TriggerRefreshForConfig([memberZoneInfo.Name]); //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zone _dnsServer.CacheZoneManager.DeleteZone(memberZoneInfo.Name); } private void AuthZoneManager_SecondaryCatalogZoneRemoved(object sender, SecondaryCatalogEventArgs e) { _authManager.RemoveAllPermissions(PermissionSection.Zones, e.ZoneInfo.Name); _authManager.SaveConfigFile(); //delete cache for this zone to allow rebuilding cache data without using the current zone _dnsServer.CacheZoneManager.DeleteZone(e.ZoneInfo.Name); } #endregion #region public public async Task StartAsync(bool throwIfBindFails = false) { if (_disposed) ObjectDisposedException.ThrowIf(_disposed, this); if (_isRunning) throw new DnsWebServiceException("The DNS web service is already running."); try { //init dns server _dnsServer = new DnsServer(_configFolder, Path.Combine(_appFolder, "dohwww"), _log); //init dhcp server _dhcpServer = new DhcpServer(Path.Combine(_configFolder, "scopes"), _log); _dhcpServer.DnsServer = _dnsServer; _dhcpServer.AuthManager = _authManager; //load web service config file LoadConfigFile(); //load dns config file _dnsServer.LoadConfigFile(); //load all dns applications await _dnsServer.DnsApplicationManager.LoadAllApplicationsAsync(); //load all zones files _dnsServer.AuthZoneManager.SecondaryCatalogZoneAdded += AuthZoneManager_SecondaryCatalogZoneAdded; _dnsServer.AuthZoneManager.SecondaryCatalogZoneRemoved += AuthZoneManager_SecondaryCatalogZoneRemoved; _dnsServer.AuthZoneManager.LoadAllZoneFiles(); InspectAndFixZonePermissions(); //disable zones from old config format if (_configDisabledZones != null) { foreach (string domain in _configDisabledZones) { AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.GetAuthZoneInfo(domain); if (zoneInfo is not null) { zoneInfo.Disabled = true; _dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } } } //load allowed zone and blocked zone files _dnsServer.AllowedZoneManager.LoadAllowedZoneFile(); _dnsServer.BlockedZoneManager.LoadBlockedZoneFile(); _dnsServer.BlockListZoneManager.LoadConfigFile(); //init cluster manager _clusterManager = new ClusterManager(this); //load cluster config file _clusterManager.LoadConfigFile(); //start web service if (throwIfBindFails) await StartWebServiceAsync(false); else await TryStartWebServiceAsync([IPAddress.Any, IPAddress.IPv6Any], 5380, 53443); //start dns and dhcp await _dnsServer.StartAsync(throwIfBindFails); _dhcpServer.Start(); _log.Write("DNS Server (v" + _currentVersion.ToString() + ") was started successfully."); _isRunning = true; } catch (Exception ex) { _log.Write("Failed to start DNS Server (v" + _currentVersion.ToString() + ")\r\n" + ex.ToString()); throw; } } public async Task StopAsync() { if (!_isRunning || _disposed) return; try { //stop cluster manager _clusterManager?.Dispose(); //stop web service await StopWebServiceAsync(); //stop dhcp _dhcpServer?.Dispose(); //stop dns & save cache to disk if (_dnsServer is not null) await _dnsServer.DisposeAsync(); _log.Write("DNS Server (v" + _currentVersion.ToString() + ") was stopped successfully."); _isRunning = false; } catch (Exception ex) { _log.Write("Failed to stop DNS Server (v" + _currentVersion.ToString() + ")\r\n" + ex.ToString()); throw; } } #endregion #region properties public DnsServer DnsServer { get { return _dnsServer; } } public DateTime UpTimeStamp { get { return _uptimestamp; } } public string ConfigFolder { get { return _configFolder; } } public int WebServiceHttpPort { get { return _webServiceHttpPort; } } public int WebServiceTlsPort { get { return _webServiceTlsPort; } } internal bool IsWebServiceTlsEnabled { get { return _webServiceEnableTls && (_webServiceUseSelfSignedTlsCertificate || !string.IsNullOrEmpty(_webServiceTlsCertificatePath)) && (_webServiceSslServerAuthenticationOptions is not null); } } internal X509Certificate2 WebServiceTlsCertificate { get { if (_webServiceSslServerAuthenticationOptions is null) return null; return _webServiceSslServerAuthenticationOptions.ServerCertificateContext.TargetCertificate; } } internal AuthManager AuthManager { get { return _authManager; } } internal LogManager LogManager { get { return _log; } } #endregion } }