Repository: TechnitiumSoftware/DnsServer Branch: master Commit: 7f7ce2685ead Files: 314 Total size: 5.2 MB Directory structure: gitextract_88vord7i/ ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── APIDOCS.md ├── Apps/ │ ├── AdvancedBlockingApp/ │ │ ├── AdvancedBlockingApp.csproj │ │ ├── App.cs │ │ └── dnsApp.config │ ├── AdvancedForwardingApp/ │ │ ├── AdvancedForwardingApp.csproj │ │ ├── App.cs │ │ ├── adguard-upstreams.txt │ │ └── dnsApp.config │ ├── AutoPtrApp/ │ │ ├── App.cs │ │ ├── AutoPtrApp.csproj │ │ └── dnsApp.config │ ├── BlockPageApp/ │ │ ├── App.cs │ │ ├── BlockPageApp.csproj │ │ ├── dnsApp.config │ │ └── wwwroot/ │ │ └── index.html │ ├── DefaultRecordsApp/ │ │ ├── App.cs │ │ ├── DefaultRecordsApp.csproj │ │ └── dnsApp.config │ ├── Dns64App/ │ │ ├── App.cs │ │ ├── Dns64App.csproj │ │ └── dnsApp.config │ ├── DnsBlockListApp/ │ │ ├── App.cs │ │ ├── DnsBlockListApp.csproj │ │ ├── dnsApp.config │ │ ├── domain-blocklist.txt │ │ └── ip-blocklist.txt │ ├── DnsRebindingProtectionApp/ │ │ ├── App.cs │ │ ├── DnsRebindingProtectionApp.csproj │ │ └── dnsApp.config │ ├── DropRequestsApp/ │ │ ├── App.cs │ │ ├── DropRequestsApp.csproj │ │ └── dnsApp.config │ ├── FailoverApp/ │ │ ├── Address.cs │ │ ├── CNAME.cs │ │ ├── EmailAlert.cs │ │ ├── FailoverApp.csproj │ │ ├── HealthCheck.cs │ │ ├── HealthCheckResponse.cs │ │ ├── HealthMonitor.cs │ │ ├── HealthService.cs │ │ ├── WebHook.cs │ │ └── dnsApp.config │ ├── FilterAaaaApp/ │ │ ├── App.cs │ │ ├── FilterAaaaApp.csproj │ │ ├── README.md │ │ └── dnsApp.config │ ├── GeoContinentApp/ │ │ ├── Address.cs │ │ ├── CNAME.cs │ │ ├── GeoContinentApp.csproj │ │ ├── MaxMind.cs │ │ ├── ReadMe.txt │ │ └── dnsApp.config │ ├── GeoCountryApp/ │ │ ├── Address.cs │ │ ├── CNAME.cs │ │ ├── GeoCountryApp.csproj │ │ ├── MaxMind.cs │ │ ├── ReadMe.txt │ │ └── dnsApp.config │ ├── GeoDistanceApp/ │ │ ├── Address.cs │ │ ├── CNAME.cs │ │ ├── GeoDistanceApp.csproj │ │ ├── MaxMind.cs │ │ ├── ReadMe.txt │ │ └── dnsApp.config │ ├── LogExporterApp/ │ │ ├── App.cs │ │ ├── AppConfig.cs │ │ ├── LogEntry.cs │ │ ├── LogExporterApp.csproj │ │ ├── Strategy/ │ │ │ ├── ExportManager.cs │ │ │ ├── FileExportStrategy.cs │ │ │ ├── HttpExportStrategy.cs │ │ │ ├── IExportStrategy.cs │ │ │ └── SyslogExportStrategy.cs │ │ └── dnsApp.config │ ├── MispConnectorApp/ │ │ ├── App.cs │ │ ├── MispConnectorApp.csproj │ │ ├── README.md │ │ └── dnsApp.config │ ├── NoDataApp/ │ │ ├── App.cs │ │ ├── NoDataApp.csproj │ │ └── dnsApp.config │ ├── NxDomainApp/ │ │ ├── App.cs │ │ ├── NxDomainApp.csproj │ │ └── dnsApp.config │ ├── NxDomainOverrideApp/ │ │ ├── App.cs │ │ ├── NxDomainOverrideApp.csproj │ │ └── dnsApp.config │ ├── QueryLogsMySqlApp/ │ │ ├── App.cs │ │ ├── QueryLogsMySqlApp.csproj │ │ └── dnsApp.config │ ├── QueryLogsSqlServerApp/ │ │ ├── App.cs │ │ ├── QueryLogsSqlServerApp.csproj │ │ └── dnsApp.config │ ├── QueryLogsSqliteApp/ │ │ ├── App.cs │ │ ├── QueryLogsSqliteApp.csproj │ │ └── dnsApp.config │ ├── SplitHorizonApp/ │ │ ├── AddressTranslation.cs │ │ ├── README.md │ │ ├── SimpleAddress.cs │ │ ├── SimpleCNAME.cs │ │ ├── SplitHorizonApp.csproj │ │ └── dnsApp.config │ ├── WeightedRoundRobinApp/ │ │ ├── Address.cs │ │ ├── CNAME.cs │ │ ├── WeightedRoundRobinApp.csproj │ │ └── dnsApp.config │ ├── WhatIsMyDnsApp/ │ │ ├── App.cs │ │ ├── WhatIsMyDnsApp.csproj │ │ └── dnsApp.config │ ├── WildIpApp/ │ │ ├── App.cs │ │ ├── WildIpApp.csproj │ │ └── dnsApp.config │ ├── ZoneAliasApp/ │ │ ├── App.cs │ │ ├── ZoneAliasApp.csproj │ │ └── dnsApp.config │ └── apps2.json ├── CHANGELOG.md ├── DnsServer.sln ├── DnsServerApp/ │ ├── DnsServerApp.csproj │ ├── Program.cs │ ├── Properties/ │ │ └── PublishProfiles/ │ │ └── FolderProfile.pubxml │ ├── install.sh │ ├── start.bat │ ├── start.sh │ ├── systemd.service │ └── uninstall.sh ├── DnsServerCore/ │ ├── Auth/ │ │ ├── AuthManager.cs │ │ ├── Group.cs │ │ ├── Permission.cs │ │ ├── User.cs │ │ └── UserSession.cs │ ├── Cluster/ │ │ ├── ClusterManager.cs │ │ ├── ClusterNode.cs │ │ └── InternalDnsClient.cs │ ├── Dhcp/ │ │ ├── DhcpMessage.cs │ │ ├── DhcpOption.cs │ │ ├── DhcpServer.cs │ │ ├── DhcpServerException.cs │ │ ├── Exclusion.cs │ │ ├── Lease.cs │ │ ├── Options/ │ │ │ ├── BroadcastAddressOption.cs │ │ │ ├── CAPWAPAccessControllerOption.cs │ │ │ ├── ClasslessStaticRouteOption.cs │ │ │ ├── ClientFullyQualifiedDomainNameOption.cs │ │ │ ├── ClientIdentifierOption.cs │ │ │ ├── DhcpMessageTypeOption.cs │ │ │ ├── DomainNameOption.cs │ │ │ ├── DomainNameServerOption.cs │ │ │ ├── DomainSearchOption.cs │ │ │ ├── HostNameOption.cs │ │ │ ├── IpAddressLeaseTimeOption.cs │ │ │ ├── MaximumDhcpMessageSizeOption.cs │ │ │ ├── NetBiosNameServerOption.cs │ │ │ ├── NetworkTimeProtocolServersOption.cs │ │ │ ├── OptionOverloadOption.cs │ │ │ ├── ParameterRequestListOption.cs │ │ │ ├── RebindingTimeValueOption.cs │ │ │ ├── RenewalTimeValueOption.cs │ │ │ ├── RequestedIpAddressOption.cs │ │ │ ├── RouterOption.cs │ │ │ ├── ServerIdentifierOption.cs │ │ │ ├── SubnetMaskOption.cs │ │ │ ├── TftpServerAddressOption.cs │ │ │ ├── VendorClassIdentifierOption.cs │ │ │ └── VendorSpecificInformationOption.cs │ │ └── Scope.cs │ ├── Dns/ │ │ ├── Applications/ │ │ │ ├── DnsApplication.cs │ │ │ ├── DnsApplicationAssemblyLoadContext.cs │ │ │ ├── DnsApplicationManager.cs │ │ │ └── InternalDnsServer.cs │ │ ├── DirectDnsClient.cs │ │ ├── DnsServer.cs │ │ ├── DnsServerException.cs │ │ ├── Dnssec/ │ │ │ ├── DnssecEcdsaPrivateKey.cs │ │ │ ├── DnssecEddsaPrivateKey.cs │ │ │ ├── DnssecPrivateKey.cs │ │ │ └── DnssecRsaPrivateKey.cs │ │ ├── ResolverDnsCache.cs │ │ ├── ResolverPrefetchDnsCache.cs │ │ ├── ResourceRecords/ │ │ │ ├── AuthRecordInfo.cs │ │ │ ├── CacheRecordInfo.cs │ │ │ ├── DnsNSRecordDataExtended.cs │ │ │ ├── DnsResourceRecordExtensions.cs │ │ │ ├── DnsSOARecordDataExtended.cs │ │ │ ├── GenericRecordInfo.cs │ │ │ ├── HistoryRecordInfo.cs │ │ │ ├── NSRecordInfo.cs │ │ │ ├── SOARecordInfo.cs │ │ │ └── SVCBRecordInfo.cs │ │ ├── StatsManager.cs │ │ ├── Trees/ │ │ │ ├── AuthZoneNode.cs │ │ │ ├── AuthZoneTree.cs │ │ │ ├── CacheZoneTree.cs │ │ │ ├── DomainTree.cs │ │ │ ├── InvalidDomainNameException.cs │ │ │ └── ZoneTree.cs │ │ ├── ZoneManagers/ │ │ │ ├── AllowedZoneManager.cs │ │ │ ├── AuthZoneManager.cs │ │ │ ├── BlockListZoneManager.cs │ │ │ ├── BlockedZoneManager.cs │ │ │ └── CacheZoneManager.cs │ │ └── Zones/ │ │ ├── ApexZone.cs │ │ ├── AuthZone.cs │ │ ├── AuthZoneInfo.cs │ │ ├── CacheZone.cs │ │ ├── CatalogSubDomainZone.cs │ │ ├── CatalogZone.cs │ │ ├── ForwarderSubDomainZone.cs │ │ ├── ForwarderZone.cs │ │ ├── PrimarySubDomainZone.cs │ │ ├── PrimaryZone.cs │ │ ├── SecondaryCatalogSubDomainZone.cs │ │ ├── SecondaryCatalogZone.cs │ │ ├── SecondaryForwarderZone.cs │ │ ├── SecondarySubDomainZone.cs │ │ ├── SecondaryZone.cs │ │ ├── StubZone.cs │ │ ├── SubDomainZone.cs │ │ └── Zone.cs │ ├── DnsServerCore.csproj │ ├── DnsWebService.cs │ ├── DnsWebServiceException.cs │ ├── DnsWebServiceLegacy.cs │ ├── Extensions.cs │ ├── InvalidTokenWebServiceException.cs │ ├── LogManager.cs │ ├── TwoFactorAuthRequiredWebServiceException.cs │ ├── WebServiceApi.cs │ ├── WebServiceAppsApi.cs │ ├── WebServiceAuthApi.cs │ ├── WebServiceClusterApi.cs │ ├── WebServiceDashboardApi.cs │ ├── WebServiceDhcpApi.cs │ ├── WebServiceLogsApi.cs │ ├── WebServiceOtherZonesApi.cs │ ├── WebServiceSettingsApi.cs │ ├── WebServiceZonesApi.cs │ ├── dohwww/ │ │ ├── css/ │ │ │ └── main.css │ │ ├── index.html │ │ ├── js/ │ │ │ └── main.js │ │ └── robots.txt │ ├── named.root │ ├── root-anchors.xml │ └── www/ │ ├── css/ │ │ └── main.css │ ├── fonts/ │ │ └── FontAwesome.otf │ ├── index.html │ ├── js/ │ │ ├── apps.js │ │ ├── auth.js │ │ ├── cluster.js │ │ ├── common.js │ │ ├── dhcp.js │ │ ├── dnsclient.js │ │ ├── logs.js │ │ ├── main.js │ │ ├── other-zones.js │ │ └── zone.js │ ├── json/ │ │ ├── dnsclient-server-list-builtin.json │ │ ├── quick-block-lists-builtin.json │ │ ├── quick-forwarders-list-builtin.json │ │ └── readme.txt │ └── robots.txt ├── DnsServerCore.ApplicationCommon/ │ ├── DnsServerCore.ApplicationCommon.csproj │ ├── IDnsAppRecordRequestHandler.cs │ ├── IDnsApplication.cs │ ├── IDnsApplicationPreference.cs │ ├── IDnsAuthoritativeRequestHandler.cs │ ├── IDnsPostProcessor.cs │ ├── IDnsQueryLogger.cs │ ├── IDnsQueryLogs.cs │ ├── IDnsRequestBlockingHandler.cs │ ├── IDnsRequestController.cs │ └── IDnsServer.cs ├── DnsServerCore.HttpApi/ │ ├── DnsServerCore.HttpApi.csproj │ ├── HttpApiClient.cs │ ├── HttpApiClientException.cs │ ├── InvalidTokenHttpApiClientException.cs │ ├── Models/ │ │ ├── ClusterInfo.cs │ │ ├── DashboardStats.cs │ │ └── SessionInfo.cs │ └── TwoFactorAuthRequiredHttpApiClientException.cs ├── DnsServerSystemTrayApp/ │ ├── DnsProvider.cs │ ├── DnsServerSystemTrayApp.csproj │ ├── MainApplicationContext.cs │ ├── NotifyIconExtension.cs │ ├── Program.cs │ ├── Properties/ │ │ ├── PublishProfiles/ │ │ │ └── FolderProfile.pubxml │ │ ├── Resources.Designer.cs │ │ └── Resources.resx │ ├── frmAbout.Designer.cs │ ├── frmAbout.cs │ ├── frmAbout.resx │ ├── frmManageDnsProviders.Designer.cs │ ├── frmManageDnsProviders.cs │ └── frmManageDnsProviders.resx ├── DnsServerWindowsService/ │ ├── DnsServerWindowsService.csproj │ ├── DnsServiceWorker.cs │ ├── Program.cs │ └── Properties/ │ └── PublishProfiles/ │ └── FolderProfile.pubxml ├── DnsServerWindowsSetup/ │ ├── DnsServerSetup.iss │ ├── appinstall.iss │ ├── dotnet.iss │ ├── helper.iss │ ├── legacy.iss │ └── service.iss ├── DockerEnvironmentVariables.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── build.md └── docker-compose.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto *.sh text eol=lf supervisor.conf text eol=lf systemd.service text eol=lf # Using the HEREDOC feature expects LF: Dockerfile text eol=lf ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: technitium # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ [Xx]64/ [Xx]86/ [Bb]uild/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Un-comment the next line if you do not want to checkin # your web deploy settings because they may include unencrypted # passwords #*.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Microsoft Azure ApplicationInsights config file ApplicationInsights.config # Windows Store app package directory AppPackages/ BundleArtifacts/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # LightSwitch generated files GeneratedArtifacts/ ModelManifest.xml # Paket dependency manager .paket/paket.exe # FAKE - F# Make .fake/ Other/ ================================================ FILE: APIDOCS.md ================================================ # Technitium DNS Server API Documentation Technitium DNS Server provides a HTTP API which is used by the web console to perform all actions. Thus any action that the web console does can be performed using this API from your own applications. The URL in the documentation uses `localhost` and port `5380`. You should use the hostname/IP address and port that is specific to your DNS server instance. ## API Request Unless it is explicitly specified, all HTTP API requests can use both `GET` or `POST` methods. When using `POST` method to pass the API parameters as form data, the `Content-Type` header must be set to `application/x-www-form-urlencoded`. When the HTTP API call is used to upload files, the call must use `POST` method and the `Content-Type` header must be set to `multipart/form-data`. Note! The "set" type of API requests will overwrite any existing value managed by that call. The "add" type of API requests will append to existing value managed by the call. ## API Response Format The HTTP API returns a JSON formatted response for all requests. The JSON object returned contains `status` property which indicate if the request was successful. The `status` property can have following values: - `ok`: This indicates that the call was successful. - `error`: This response tells the call failed and provides additional properties that provide details about the error. - `invalid-token`: When a session has expired or an invalid token was provided this response is received. - `2fa-required`: When a user has two-factor authentication enabled and the OTP was not provided during login, change password, etc. API calls. A successful response will look as shown below. Note that there will be other properties in the response which are specific to the request that was made. ``` { "status": "ok" } ``` In case of errors, the response will look as shown below. The `errorMessage` property can be shown in the UI to the user while the other two properties are useful for debugging. ``` { "status": "error", "errorMessage": "error message", "stackTrace": "application stack trace", "innerErrorMessage": "inner exception message" } ``` ## Name Server Address Format The DNS server uses a specific text format to define the name server address to allow specifying multiple parameters like the domain name, IP address, port or URL. This format is used in the web console as well as in this API. It is used to specify forwarder address in DNS settings, conditional forwarder zone's FWD record, or the server address in DNS Client resolve query API calls. - A name server address with just an IP address is specified just as its string literal with optional port number is as shown: `1.1.1.1` or `8.8.8.8:53`. When port is not specified, the default port number for the selected DNS transport protocol is used. - A name server address with just a domain name is specified similarly as its string literal with optional port number is as shown: `dns.quad9.net:853` or `cloudflare-dns.com`. When port is not specified, the default port number for the selected DNS transport protocol is used. - A combination of domain name and IP address together with optional port number is as shown: `cloudflare-dns.com (1.1.1.1)`, `dns.quad9.net (9.9.9.9:853)` or `dns.quad9.net:853 (9.9.9.9)`. Here, the domain name (with optional port number) is specified and the IP address (with optional port number) is specified in a round bracket. When port is not specified, the default port number for the selected DNS transport protocol is used. This allows the DNS server to use the specified IP address instead of trying to resolve it separately. - A name server address that specifies a DNS-over-HTTPS URL is specified just as its string literal is as shown: `https://cloudflare-dns.com/dns-query` - A combination of DNS-over-HTTPS URL and IP address together is as shown: `https://cloudflare-dns.com/dns-query (1.1.1.1)`. Here, the IP address of the domain name in the URL is specified in the round brackets. This allows the DNS server to use the specified IP address instead of trying to resolve it separately. - IPv6 addresses must always be enclosed in square brackets when port is specified as shown: `cloudflare-dns.com ([2606:4700:4700::1111]:853)` or `[2606:4700:4700::1111]:853` ## User API Calls These API calls allow to a user to login, logout, perform account management, etc. Once logged in, a session token is returned which MUST be used with all other API calls. ### Login This call authenticates with the server and generates a session token to be used for subsequent API calls. The session token expires as per the user's session expiry timeout value (default 30 minutes) from the last API call. URL:\ `http://localhost:5380/api/user/login?user=admin&pass=admin&includeInfo=true` OBSOLETE PATH:\ `/api/login` PERMISSIONS:\ None WHERE: - `user`: The username for the user account. The built-in administrator username on the DNS server is `admin`. - `pass`: The password for the user account. The default password for `admin` user is `admin`. - `totp` (optional): The time-based one-time password for the user account if it has Two Factor Authentication (2FA) enabled. - `includeInfo` (optional): Includes basic info relevant for the user in response. WARNING: It is highly recommended to change the password on first use to avoid security related issues. RESPONSE: ``` { "displayName": "Administrator", "username": "admin", "totpEnabled": false, "token": "932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9", "info": { "version": "14.3", "dnsServerDomain": "server1", "defaultRecordTtl": 3600, "defaultNsRecordTtl": 14400, "defaultSoaRecordTtl": 900, "permissions": { "Dashboard": { "canView": true, "canModify": true, "canDelete": true }, "Zones": { "canView": true, "canModify": true, "canDelete": true }, "Cache": { "canView": true, "canModify": true, "canDelete": true }, "Allowed": { "canView": true, "canModify": true, "canDelete": true }, "Blocked": { "canView": true, "canModify": true, "canDelete": true }, "Apps": { "canView": true, "canModify": true, "canDelete": true }, "DnsClient": { "canView": true, "canModify": true, "canDelete": true }, "Settings": { "canView": true, "canModify": true, "canDelete": true }, "DhcpServer": { "canView": true, "canModify": true, "canDelete": true }, "Administration": { "canView": true, "canModify": true, "canDelete": true }, "Logs": { "canView": true, "canModify": true, "canDelete": true } } }, "status": "ok" } ``` WHERE: - `token`: Is the session token generated that MUST be used with all subsequent API calls. ### Create API Token Allows creating a non-expiring API token that can be used with automation scripts to make API calls. The token allows access to API calls with the same privileges as that of the user account. Thus its recommended to create a separate user account with limited permissions as required by the specific task that the token will be used for. The token cannot be used to change the user's password, or update the user profile details. URL:\ `http://localhost:5380/api/user/createToken?user=admin&pass=admin&tokenName=MyToken1` PERMISSIONS:\ None WHERE: - `user`: The username for the user account for which to generate the API token. - `pass`: The password for the user account. - `totp` (optional): The time-based one-time password for the user account if it has Two Factor Authentication (2FA) enabled. - `tokenName`: The name of the created token to identify its session. RESPONSE: ``` { "username": "admin", "tokenName": "MyToken1", "token": "932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9", "status": "ok" } ``` WHERE: - `token`: Is the session token generated that MUST be used with all subsequent API calls. ### Logout This call ends the session generated by the `login` or the `createToken` call. The `token` would no longer be valid after calling the `logout` API. URL:\ `http://localhost:5380/api/user/logout?token=932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9` OBSOLETE PATH:\ `/api/logout` PERMISSIONS:\ None WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "status": "ok" } ``` ### Get Session Info Returns the same info as that of the `login` or the `createToken` calls for the session specified by the token. URL:\ `http://localhost:5380/api/user/session/get?token=932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9` PERMISSIONS:\ None WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "displayName": "Administrator", "username": "admin", "totpEnabled": false, "token": "932b2a3495852c15af01598f62563ae534460388b6a370bfbbb8bb6094b698e9", "info": { "version": "14.0", "uptimestamp": "2023-07-29T08:01:31.1117463Z", "dnsServerDomain": "server1.example.com", "clusterInitialized": true, "clusterDomain": "example.com" "defaultRecordTtl": 3600, "useSoaSerialDateScheme": false, "dnssecValidation": true, "permissions": { "Dashboard": { "canView": true, "canModify": true, "canDelete": true }, "Zones": { "canView": true, "canModify": true, "canDelete": true }, "Cache": { "canView": true, "canModify": true, "canDelete": true }, "Allowed": { "canView": true, "canModify": true, "canDelete": true }, "Blocked": { "canView": true, "canModify": true, "canDelete": true }, "Apps": { "canView": true, "canModify": true, "canDelete": true }, "DnsClient": { "canView": true, "canModify": true, "canDelete": true }, "Settings": { "canView": true, "canModify": true, "canDelete": true }, "DhcpServer": { "canView": true, "canModify": true, "canDelete": true }, "Administration": { "canView": true, "canModify": true, "canDelete": true }, "Logs": { "canView": true, "canModify": true, "canDelete": true } } }, "status": "ok" } ``` ### Delete User Session Allows deleting a session for the current user. URL:\ `http://localhost:5380/api/user/session/delete?token=x&partialToken=620c3bfcd09d0a07` PERMISSIONS:\ None WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `partialToken`: The partial token as returned by the user profile details API call. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Change Password Allows changing the password for the current logged in user account. NOTE: It is highly recommended to change the `admin` user password on first use to avoid security related issues. URL:\ `http://localhost:5380/api/user/changePassword?token=x&pass=password&newPass=newpassword` OBSOLETE PATH:\ `/api/changePassword` PERMISSIONS:\ None WHERE: - `token`: The session token generated only by the `login` call. - `pass`: The current password for the currently logged in user. - `newPass`: The new password to be set for the currently logged in user. - `totp` (optional): The 6-digit code from the authenticator app if the user has 2FA enabled. - `iterations` (optional): The number of iterations for PBKDF2 SHA256 password hashing. RESPONSE: ``` { "status": "ok" } ``` ### Initialize 2FA Initializes two-factor authentication for the current logged in user account. The secret returned by this API call needs to be used with authenticator apps like microsoft Authenticator or Google Authenticator. This call is the first step to enable 2FA followed by calling the Enable 2FA API call. URL:\ `http://localhost:5380/api/user/2fa/init?token=x` PERMISSIONS:\ None WHERE: - `token`: The session token generated only by the `login` call. RESPONSE: ``` { "response": { "totpEnabled": false, "qrCodePngImage": "iVBORw0KGgoAAAANSUhEU...", "secret": "RZ56CYOXKAXI5D23" }, "status": "ok" } ``` ### Enable 2FA Enables two-factor authentication for the current logged in user account. This API call can be called only after the Initialize 2FA API call. URL:\ `http://localhost:5380/api/user/2fa/enable?token=x` PERMISSIONS:\ None WHERE: - `token`: The session token generated only by the `login` call. - `totp`: The 6-digit code from the authenticator app. RESPONSE: ``` { "status": "ok" } ``` ### Disable 2FA Disables two-factor authentication for the current logged in user account. URL:\ `http://localhost:5380/api/user/2fa/disable?token=x` PERMISSIONS:\ None WHERE: - `token`: The session token generated only by the `login` call. RESPONSE: ``` { "status": "ok" } ``` ### Get User Profile Details Gets the user account profile details. URL:\ `http://localhost:5380/api/user/profile/get?token=x` PERMISSIONS:\ None WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "response": { "displayName": "Administrator", "username": "admin", "totpEnabled": false, "disabled": false, "previousSessionLoggedOn": "2022-09-15T12:59:05.944Z", "previousSessionRemoteAddress": "127.0.0.1", "recentSessionLoggedOn": "2022-09-15T13:57:50.1843973Z", "recentSessionRemoteAddress": "127.0.0.1", "sessionTimeoutSeconds": 1800, "memberOfGroups": [ "Administrators" ], "sessions": [ { "username": "admin", "isCurrentSession": true, "partialToken": "620c3bfcd09d0a07", "type": "Standard", "tokenName": null, "lastSeen": "2022-09-15T13:58:02.4728Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" } ] }, "status": "ok" } ``` ### Set User Profile Details Allows changing user account profile values. URL:\ `http://localhost:5380/api/user/profile/set?token=x&displayName=Administrator&sessionTimeoutSeconds=1800` PERMISSIONS:\ None WHERE: - `token`: The session token generated only by the `login` call. - `displayName` (optional): The display name to set for the user account. - `sessionTimeoutSeconds` (optional): The session timeout value to set in seconds for the user account. RESPONSE: ``` { "response": { "displayName": "Administrator", "username": "admin", "totpEnabled": false, "disabled": false, "previousSessionLoggedOn": "2022-09-15T12:59:05.944Z", "previousSessionRemoteAddress": "127.0.0.1", "recentSessionLoggedOn": "2022-09-15T13:57:50.1843973Z", "recentSessionRemoteAddress": "127.0.0.1", "sessionTimeoutSeconds": 1800, "memberOfGroups": [ "Administrators" ], "sessions": [ { "username": "admin", "isCurrentSession": true, "partialToken": "620c3bfcd09d0a07", "type": "Standard", "tokenName": null, "lastSeen": "2022-09-15T14:00:50.288738Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" } ] }, "status": "ok" } ``` ### Check For Update This call requests the server to check for software update. URL:\ `http://localhost:5380/api/user/checkForUpdate?token=x` OBSOLETE PATH:\ `/api/checkForUpdate` PERMISSIONS:\ None WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "response": { "updateAvailable": true, "updateVersion": "9.0", "currentVersion": "8.1.4", "updateTitle": "New Update Available!", "updateMessage": "Follow the instructions from the link below to update the DNS server to the latest version. Read the change logs before installing the update to know if there are any breaking changes.", "downloadLink": "https://download.technitium.com/dns/DnsServerSetup.zip", "instructionsLink": "https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html", "changeLogLink": "https://github.com/TechnitiumSoftware/DnsServer/blob/master/CHANGELOG.md" }, "status": "ok" } ``` ## Dashboard API Calls These API calls provide access to dashboard stats and allow deleting stat files. ### Get Stats Returns the DNS stats that are displayed on the web console dashboard. URL:\ `http://localhost:5380/api/dashboard/stats/get?token=x&type=LastHour&utc=true` OBSOLETE PATH:\ `api/getStats` PERMISSIONS:\ Dashboard: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node`: The node domain name for which the stats data is needed. When unspecified, the current node is used. Set node name as `cluster` to get aggregate stats for the entire cluster. This parameter can be used only when Clustering is initialized. - `type` (optional): The duration type for which valid values are: [`LastHour`, `LastDay`, `LastWeek`, `LastMonth`, `LastYear`, `Custom`]. Default value is `LastHour`. - `utc` (optional): Set to `true` to return the main chart data with labels in UTC date time format using which the labels can be converted into local time for display using the received `labelFormat`. - `dontTrimQueryTypeData` (optional): Set to `true` to get full data for query type chart instead of top 10 entries. Default value is `false` when unspecified. - `start` (optional): The start date in ISO 8601 format. Applies only to `custom` type. - `end` (optional): The end date in ISO 8601 format. Applies only to `custom` type. RESPONSE: ``` { "response": { "stats": { "totalQueries": 925, "totalNoError": 834, "totalServerFailure": 1, "totalNxDomain": 90, "totalRefused": 0, "totalAuthoritative": 47, "totalRecursive": 348, "totalCached": 481, "totalBlocked": 49, "totalDropped": 0, "totalClients": 6, "zones": 19, "cachedEntries": 6330, "allowedZones": 10, "blockedZones": 1, "allowListZones": 0, "blockListZones": 307447 }, "mainChartData": { "labelFormat": "HH:mm", "labels": [ "2024-02-04T10:38:00.0000000Z", "2024-02-04T10:39:00.0000000Z", "2024-02-04T10:40:00.0000000Z", "2024-02-04T10:41:00.0000000Z", "2024-02-04T10:42:00.0000000Z", "2024-02-04T10:43:00.0000000Z", "2024-02-04T10:44:00.0000000Z", "2024-02-04T10:45:00.0000000Z", "2024-02-04T10:46:00.0000000Z", "2024-02-04T10:47:00.0000000Z", "2024-02-04T10:48:00.0000000Z", "2024-02-04T10:49:00.0000000Z", "2024-02-04T10:50:00.0000000Z", "2024-02-04T10:51:00.0000000Z", "2024-02-04T10:52:00.0000000Z", "2024-02-04T10:53:00.0000000Z", "2024-02-04T10:54:00.0000000Z", "2024-02-04T10:55:00.0000000Z", "2024-02-04T10:56:00.0000000Z", "2024-02-04T10:57:00.0000000Z", "2024-02-04T10:58:00.0000000Z", "2024-02-04T10:59:00.0000000Z", "2024-02-04T11:00:00.0000000Z", "2024-02-04T11:01:00.0000000Z", "2024-02-04T11:02:00.0000000Z", "2024-02-04T11:03:00.0000000Z", "2024-02-04T11:04:00.0000000Z", "2024-02-04T11:05:00.0000000Z", "2024-02-04T11:06:00.0000000Z", "2024-02-04T11:07:00.0000000Z", "2024-02-04T11:08:00.0000000Z", "2024-02-04T11:09:00.0000000Z", "2024-02-04T11:10:00.0000000Z", "2024-02-04T11:11:00.0000000Z", "2024-02-04T11:12:00.0000000Z", "2024-02-04T11:13:00.0000000Z", "2024-02-04T11:14:00.0000000Z", "2024-02-04T11:15:00.0000000Z", "2024-02-04T11:16:00.0000000Z", "2024-02-04T11:17:00.0000000Z", "2024-02-04T11:18:00.0000000Z", "2024-02-04T11:19:00.0000000Z", "2024-02-04T11:20:00.0000000Z", "2024-02-04T11:21:00.0000000Z", "2024-02-04T11:22:00.0000000Z", "2024-02-04T11:23:00.0000000Z", "2024-02-04T11:24:00.0000000Z", "2024-02-04T11:25:00.0000000Z", "2024-02-04T11:26:00.0000000Z", "2024-02-04T11:27:00.0000000Z", "2024-02-04T11:28:00.0000000Z", "2024-02-04T11:29:00.0000000Z", "2024-02-04T11:30:00.0000000Z", "2024-02-04T11:31:00.0000000Z", "2024-02-04T11:32:00.0000000Z", "2024-02-04T11:33:00.0000000Z", "2024-02-04T11:34:00.0000000Z", "2024-02-04T11:35:00.0000000Z", "2024-02-04T11:36:00.0000000Z", "2024-02-04T11:37:00.0000000Z" ], "datasets": [ { "label": "Total", "backgroundColor": "rgba(102, 153, 255, 0.1)", "borderColor": "rgb(102, 153, 255)", "borderWidth": 2, "fill": true, "data": [ 4, 6, 13, 9, 27, 9, 11, 15, 9, 10, 5, 9, 5, 17, 6, 9, 61, 23, 9, 21, 8, 20, 5, 7, 35, 26, 33, 20, 7, 12, 4, 14, 3, 19, 37, 10, 18, 12, 7, 30, 47, 16, 10, 3, 12, 11, 37, 3, 18, 22, 16, 6, 15, 5, 41, 13, 7, 9, 17, 12 ] }, { "label": "No Error", "backgroundColor": "rgba(92, 184, 92, 0.1)", "borderColor": "rgb(92, 184, 92)", "borderWidth": 2, "fill": true, "data": [ 4, 6, 11, 9, 22, 6, 11, 13, 9, 7, 5, 7, 4, 13, 5, 7, 59, 22, 8, 21, 8, 19, 5, 7, 31, 25, 24, 16, 6, 12, 4, 12, 3, 19, 36, 8, 18, 12, 7, 28, 46, 16, 10, 3, 11, 10, 34, 2, 10, 13, 11, 6, 15, 5, 40, 12, 6, 9, 14, 12 ] }, { "label": "Server Failure", "backgroundColor": "rgba(217, 83, 79, 0.1)", "borderColor": "rgb(217, 83, 79)", "borderWidth": 2, "fill": true, "data": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }, { "label": "NX Domain", "backgroundColor": "rgba(120, 120, 120, 0.1)", "borderColor": "rgb(120, 120, 120)", "borderWidth": 2, "fill": true, "data": [ 0, 0, 2, 0, 5, 3, 0, 2, 0, 3, 0, 2, 1, 4, 1, 2, 2, 1, 1, 0, 0, 1, 0, 0, 4, 1, 9, 4, 1, 0, 0, 2, 0, 0, 1, 2, 0, 0, 0, 2, 0, 0, 0, 0, 1, 1, 3, 1, 8, 9, 5, 0, 0, 0, 1, 1, 1, 0, 3, 0 ] }, { "label": "Refused", "backgroundColor": "rgba(91, 192, 222, 0.1)", "borderColor": "rgb(91, 192, 222)", "borderWidth": 2, "fill": true, "data": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }, { "label": "Authoritative", "backgroundColor": "rgba(150, 150, 0, 0.1)", "borderColor": "rgb(150, 150, 0)", "borderWidth": 2, "fill": true, "data": [ 0, 0, 1, 0, 3, 1, 0, 2, 1, 1, 0, 1, 0, 2, 1, 1, 1, 1, 2, 0, 0, 1, 0, 0, 2, 1, 3, 2, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 4, 4, 2, 0, 2, 0, 1, 1, 0, 0, 0, 0 ] }, { "label": "Recursive", "backgroundColor": "rgba(23, 162, 184, 0.1)", "borderColor": "rgb(23, 162, 184)", "borderWidth": 2, "fill": true, "data": [ 1, 3, 0, 1, 14, 2, 8, 4, 1, 3, 2, 3, 1, 3, 3, 3, 36, 8, 2, 14, 4, 6, 1, 1, 26, 17, 11, 10, 2, 4, 0, 8, 1, 7, 18, 0, 3, 0, 2, 10, 6, 1, 3, 1, 5, 4, 20, 0, 5, 7, 0, 3, 7, 0, 21, 7, 1, 3, 6, 5 ] }, { "label": "Cached", "backgroundColor": "rgba(111, 84, 153, 0.1)", "borderColor": "rgb(111, 84, 153)", "borderWidth": 2, "fill": true, "data": [ 3, 3, 11, 8, 8, 4, 3, 8, 7, 4, 3, 4, 3, 10, 2, 4, 23, 14, 5, 7, 4, 13, 4, 6, 5, 8, 13, 6, 4, 8, 4, 4, 2, 12, 18, 8, 15, 12, 5, 18, 41, 15, 7, 2, 6, 6, 14, 2, 5, 6, 10, 3, 6, 5, 19, 5, 5, 6, 8, 7 ] }, { "label": "Blocked", "backgroundColor": "rgba(255, 165, 0, 0.1)", "borderColor": "rgb(255, 165, 0)", "borderWidth": 2, "fill": true, "data": [ 0, 0, 1, 0, 2, 2, 0, 1, 0, 2, 0, 1, 1, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 2, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 1, 4, 5, 4, 0, 0, 0, 0, 0, 1, 0, 3, 0 ] }, { "label": "Dropped", "backgroundColor": "rgba(30, 30, 30, 0.1)", "borderColor": "rgb(30, 30, 30)", "borderWidth": 2, "fill": true, "data": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }, { "label": "Clients", "backgroundColor": "rgba(51, 122, 183, 0.1)", "borderColor": "rgb(51, 122, 183)", "borderWidth": 2, "fill": true, "data": [ 3, 4, 3, 2, 3, 4, 2, 3, 3, 2, 2, 3, 3, 3, 2, 4, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 2, 2, 2, 3, 2, 2, 3, 3, 1, 3, 3, 3, 3, 3, 3, 2, 3, 3, 4, 2, 3, 4, 4, 2, 4, 3, 3, 3, 3, 2, 2, 3 ] } ] }, "queryResponseChartData": { "labels": [ "Authoritative", "Recursive", "Cached", "Blocked", "Dropped" ], "datasets": [ { "data": [ 47, 348, 481, 49, 0 ], "backgroundColor": [ "rgba(150, 150, 0, 0.5)", "rgba(23, 162, 184, 0.5)", "rgba(111, 84, 153, 0.5)", "rgba(255, 165, 0, 0.5)", "rgba(7, 7, 7, 0.5)" ] } ] }, "queryTypeChartData": { "labels": [ "A", "HTTPS", "AAAA", "SOA", "SRV" ], "datasets": [ { "data": [ 683, 196, 42, 2, 2 ], "backgroundColor": [ "rgba(102, 153, 255, 0.5)", "rgba(92, 184, 92, 0.5)", "rgba(7, 7, 7, 0.5)", "rgba(91, 192, 222, 0.5)", "rgba(150, 150, 0, 0.5)", "rgba(23, 162, 184, 0.5)", "rgba(111, 84, 153, 0.5)", "rgba(255, 165, 0, 0.5)", "rgba(51, 122, 183, 0.5)", "rgba(150, 150, 150, 0.5)" ] } ] }, "protocolTypeChartData": { "labels": [ "Udp" ], "datasets": [ { "data": [ 925 ], "backgroundColor": [ "rgba(111, 84, 153, 0.5)", "rgba(150, 150, 0, 0.5)", "rgba(23, 162, 184, 0.5)", "rgba(255, 165, 0, 0.5)", "rgba(91, 192, 222, 0.5)" ] } ] }, "topClients": [ { "name": "192.168.10.5", "domain": "server1.home", "hits": 463, "rateLimited": false }, { "name": "192.168.10.12", "domain": "vostro1.home", "hits": 236, "rateLimited": false }, { "name": "192.168.10.13", "hits": 165, "rateLimited": false }, { "name": "192.168.10.11", "domain": "shreyas-zare.home", "hits": 53, "rateLimited": false }, { "name": "192.168.10.15", "domain": "android-9c3d70b130d99b94.home", "hits": 6, "rateLimited": false }, { "name": "192.168.10.2", "domain": "pi1.home", "hits": 2, "rateLimited": false } ], "topDomains": [ { "name": "hses7-vod-cf-ace.cdn.hotstar.com", "hits": 114 }, { "name": "bifrost-api.hotstar.com", "hits": 61 }, { "name": "edge.microsoft.com", "hits": 52 }, { "name": "www.google.com", "hits": 34 }, { "name": "www.hotstar.com", "hits": 24 }, { "name": "safebrowsing.googleapis.com", "hits": 15 }, { "name": "www.bing.com", "hits": 14 }, { "name": "go.microsoft.com", "hits": 14 }, { "name": "graph.facebook.com", "hits": 13 }, { "name": "substrate.office.com", "hits": 11 } ], "topBlockedDomains": [ { "name": "mobile.pipe.aria.microsoft.com", "hits": 10 }, { "name": "in.api.glance.inmobi.com", "hits": 9 }, { "name": "in.analytics.glance.inmobi.com", "hits": 6 }, { "name": "app-measurement.com", "hits": 4 }, { "name": "googleads.g.doubleclick.net", "hits": 4 }, { "name": "analytics.swiggy.com", "hits": 2 }, { "name": "firebase-settings.crashlytics.com", "hits": 2 }, { "name": "cdn.cookielaw.org", "hits": 2 }, { "name": "m.urbancompany.com", "hits": 1 }, { "name": "beacons.gvt2.com", "hits": 1 } ] }, "status": "ok" } ``` ### Get Top Stats Returns the top stats data for specified stats type. URL:\ `http://localhost:5380/api/dashboard/stats/getTop?token=x&type=LastHour&statsType=TopClients&limit=1000` OBSOLETE PATH:\ `/api/getTopStats` PERMISSIONS:\ Dashboard: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node`: The node domain name for which the stats data is needed. When unspecified, the current node is used. Set node name as `cluster` to get aggregate stats for the entire cluster. This parameter can be used only when Clustering is initialized. - `type` (optional): The duration type for which valid values are: [`LastHour`, `LastDay`, `LastWeek`, `LastMonth`, `LastYear`, `custom`]. Default value is `LastHour`. - `start` (optional): The start date in ISO 8601 format. Applies only to `custom` type. - `end` (optional): The end date in ISO 8601 format. Applies only to `custom` type. - `statsType`: The stats type for which valid values are : [`TopClients`, `TopDomains`, `TopBlockedDomains`] - `limit` (optional): The limit of records to return. Default value is `1000`. - `noReverseLookup` (optional): Set to `true` to disable reverse lookup for Top Clients list. This option is only applicable with `TopClients` stats type. - `onlyRateLimitedClients` (optional): Set to `true` to list only clients which are being rate limited in the Top Clients list. This option is only applicable with `TopClients` stats type. RESPONSE: The response json will include the object with definition same in the `getStats` response depending on the `statsType`. For example below is the response for `TopClients`: ``` { "response": { "topClients": [ { "name": "192.168.10.5", "domain": "server1.local", "hits": 236, "rateLimited": false }, { "name": "192.168.10.4", "domain": "nas1.local", "hits": 16, "rateLimited": false }, { "name": "192.168.10.6", "domain": "server2.local", "hits": 14, "rateLimited": false }, { "name": "192.168.10.3", "domain": "nas2.local", "hits": 12, "rateLimited": false }, { "name": "217.31.193.175", "domain": "condor175.knot-resolver.cz", "hits": 10, "rateLimited": false }, { "name": "162.158.180.45", "hits": 9, "rateLimited": false }, { "name": "217.31.193.163", "domain": "gondor-resolver.labs.nic.cz", "hits": 9, "rateLimited": false }, { "name": "210.245.24.68", "hits": 8, "rateLimited": false }, { "name": "101.91.16.140", "hits": 8, "rateLimited": false } ], }, "status": "ok" } ``` ### Delete All Stats Permanently delete all hourly and daily stats files from the disk and clears all stats stored in memory. This call will clear all stats from the Dashboard. URL:\ `http://localhost:5380/api/dashboard/stats/deleteAll?token=x` OBSOLETE PATH:\ `/api/deleteAllStats` PERMISSIONS:\ Dashboard: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node`: The node domain name for which the stats data needs to be deleted. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ## Authoritative Zone API Calls These API calls allow managing all hosted zones on the DNS server. ### List Zones List all authoritative zones hosted on this DNS server. The list contains only the zones that the user has View permissions for. These API calls requires permission for both the Zones section as well as the individual permission for each zone. URL:\ `http://localhost:5380/api/zones/list?token=x&pageNumber=1&zonesPerPage=10` OBSOLETE PATH:\ `/api/zone/list`\ `/api/listZones` PERMISSIONS:\ Zones: View\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `pageNumber` (optional): When this parameter is specified, the API will return paginated results based on the page number and zones per pages options. When not specified, the API will return a list of all zones. - `zonesPerPage` (optional): The number of zones per page to be returned. This option is only used when `pageNumber` options is specified. The default value is `10` when not specified. RESPONSE: ``` { "response": { "pageNumber": 1, "totalPages": 2, "totalZones": 12, "zones": [ { "name": "", "type": "Secondary", "dnssecStatus": "SignedWithNSEC", "soaSerial": 1, "expiry": "2022-02-26T07:57:08.1842183Z", "isExpired": false, "syncFailed": false, "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "0.in-addr.arpa", "type": "Primary", "internal": true, "dnssecStatus": "Unsigned", "soaSerial": 1, "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", "type": "Primary", "internal": true, "dnssecStatus": "Unsigned", "soaSerial": 1, "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "127.in-addr.arpa", "type": "Primary", "internal": true, "dnssecStatus": "Unsigned", "soaSerial": 1, "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "255.in-addr.arpa", "type": "Primary", "internal": true, "dnssecStatus": "Unsigned", "soaSerial": 1, "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "example.com", "type": "Primary", "internal": false, "dnssecStatus": "SignedWithNSEC", "soaSerial": 1, "notifyFailed": false, "notifyFailedFor": [], "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "localhost", "type": "Primary", "internal": true, "dnssecStatus": "Unsigned", "soaSerial": 1, "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "test0.com", "type": "Primary", "internal": false, "dnssecStatus": "Unsigned", "soaSerial": 1, "notifyFailed": false, "notifyFailedFor": [], "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "test1.com", "type": "Primary", "internal": false, "dnssecStatus": "Unsigned", "soaSerial": 1, "notifyFailed": false, "notifyFailedFor": [], "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false }, { "name": "test2.com", "type": "Primary", "internal": false, "dnssecStatus": "Unsigned", "soaSerial": 1, "notifyFailed": false, "notifyFailedFor": [], "lastModified": "2022-02-26T07:57:08.1842183Z", "disabled": false } ] }, "status": "ok" } ``` ### List Catalog Zones Returns a list of Catalog zone names. URL:\ `http://localhost:5380/api/zones/catalogs/list?token=x` PERMISSIONS:\ Zones: View\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "catalogZoneNames": [ "catalog1" ] }, "status": "ok" } ``` ### Create Zone Creates a new authoritative zone. URL:\ `http://localhost:5380/api/zones/create?token=x&zone=example.com&type=Primary` OBSOLETE PATH:\ `/api/zone/create`\ `/api/createZone` PERMISSIONS:\ Zones: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name for creating the new zone. The value can be valid domain name, an IP address, or an network address in CIDR format. When value is IP address or network address, a reverse zone is created. - `type`: The type of zone to be created. Valid values are [`Primary`, `Secondary`, `Stub`, `Forwarder`, `SecondaryForwarder`, `Catalog`, `SecondaryCatalog`]. - `catalog` (optional): The name of the catalog zone to become its member zone. This option is valid only for `Primary`, `Secondary`, `Stub`, and `Forwarder` zones. - `useSoaSerialDateScheme` (optional): Set value to `true` to enable using date scheme for SOA serial. This optional parameter is used only with `Primary`, `Forwarder`, and `Catalog` zones. Default value is `false`. - `primaryNameServerAddresses` (optional): List of comma separated IP addresses or domain names of the primary name server. This optional parameter is used only with `Secondary`, `SecondaryForwarder`, `SecondaryCatalog`, and `Stub` zones. If this parameter is not used, the DNS server will try to recursively resolve the primary name server addresses automatically for `Secondary` and `Stub` zones. This option is required for `SecondaryForwarder` and `SecondaryCatalog` zones. - `zoneTransferProtocol` (optional): The zone transfer protocol to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones. Valid values are [`Tcp`, `Tls`, `Quic`]. - `tsigKeyName` (optional): The TSIG key name to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones. - `validateZone` (optional): Set value as `true` to enable ZONEMD validation. When enabled, the `Secondary` zone will be validated using the ZONEMD record after every zone transfer. The zone will get disabled if the validation fails. The zone must be DNSSEC signed for the validation to work. This option is only valid for `Secondary` zones. - `initializeForwarder` (optional): Set value as `true` to initialize the Conditional Forwarder zone with an FWD record or set it to `false` to create an empty `Forwarder` zone. Default value is `true`. - `protocol` (optional): The DNS transport protocol to be used by the Conditional Forwarder zone. This optional parameter is used with Conditional Forwarder zones. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. Default `Udp` protocol is used when this parameter is missing. The `initializeForwarder` parameter must be set to `true` to use this option. - `forwarder` (optional): The address of the DNS server to be used as a forwarder. This optional parameter is required to be used with Conditional Forwarder zones. A special value `this-server` can be used as a forwarder which when used will forward all the requests internally to this DNS server such that you can override the zone with records and rest of the zone gets resolved via This Server. The `initializeForwarder` parameter must be set to `true` to use this option. - `dnssecValidation` (optional): Set this boolean value to indicate if DNSSEC validation must be done. This optional parameter is required to be used with Conditional Forwarder zones. The `initializeForwarder` parameter must be set to `true` to use this option. - `proxyType` (optional): The type of proxy that must be used for conditional forwarding. This optional parameter is required to be used with Conditional Forwarder zones. Valid values are [`NoProxy`, `DefaultProxy`, `Http`, `Socks5`]. Default value `DefaultProxy` is used when this parameter is missing. The `initializeForwarder` parameter must be set to `true` to use this option. - `proxyAddress` (optional): The proxy server address to use when `proxyType` is configured. This optional parameter is required to be used with Conditional Forwarder zones. The `initializeForwarder` parameter must be set to `true` to use this option. - `proxyPort` (optional): The proxy server port to use when `proxyType` is configured. This optional parameter is required to be used with Conditional Forwarder zones. The `initializeForwarder` parameter must be set to `true` to use this option. - `proxyUsername` (optional): The proxy server username to use when `proxyType` is configured. This optional parameter is required to be used with Conditional Forwarder zones. The `initializeForwarder` parameter must be set to `true` to use this option. - `proxyPassword` (optional): The proxy server password to use when `proxyType` is configured. This optional parameter is required to be used with Conditional Forwarder zones. The `initializeForwarder` parameter must be set to `true` to use this option. REQUEST: To import a zone file while creating a `Primary` or `Forwarder` zone, use POST request with multi-part form data containing the zone file data. RESPONSE: ``` { "response": { "domain": "example.com" }, "status": "ok" } ``` WHERE: - `domain`: Will contain the zone that was created. This is specifically useful to know the reverse zone that was created. ### Import Zone Allows importing a complete zone file or a set of DNS resource records in standard RFC 1035 zone file format. URL:\ `http://localhost:5380/api/zones/import?token=x&zone=example.com&overwrite=true&overwriteSoaSerial=false` PERMISSIONS:\ Zones: Modify Zone: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to import. - `overwrite` (optional): Set to `true` to allow overwriting existing resource record set for the records being imported. - `overwriteSoaSerial` (optional): Set it to `true` to overwrite existing SOA record serial with the imported SOA record serial. Warning! Overwrite SOA serial option when used to set a lower SOA serial value than the current SOA serial will cause secondary zones to fail to sync. REQUEST: This is a POST request call where the request must use `text/plain` content type with request body containing the zone file data OR the request must be multi-part form data with the zone file data. RESPONSE: ``` { "status": "ok" } ``` ### Export Zone Exports the complete zone in standard RFC 1035 zone file format. URL:\ `http://localhost:5380/api/zones/export?token=x&zone=example.com` PERMISSIONS:\ Zones: View Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to export. RESPONSE: Response is a downloadable text file with `Content-Type: text/plain` and `Content-Disposition: attachment`. ### Clone Zone Clones an existing zone with all the records to create a new zone. URL:\ `http://localhost:5380/api/zones/clone?token=x&zone=example.com&sourceZone=template.com` PERMISSIONS:\ Zones: Modify Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to be created. - `sourceZone`: The domain name of the zone to be cloned. RESPONSE: ``` { "status": "ok" } ``` ### Convert Zone Type Converts zone from one type to another. URL:\ `http://localhost:5380/api/zones/convert?token=x&zone=example.com&type=Primary` PERMISSIONS:\ Zones: Delete Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to be converted. - `type`: The zone type to convert the current zone to. RESPONSE: ``` { "status": "ok" } ``` ### Enable Zone Enables an authoritative zone. URL:\ `http://localhost:5380/api/zones/enable?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/enable`\ `/api/enableZone` PERMISSIONS:\ Zones: Modify\ Zone: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to be enabled. RESPONSE: ``` { "status": "ok" } ``` ### Disable Zone Disables an authoritative zone. This will prevent the DNS server from responding for queries to this zone. URL:\ `http://localhost:5380/api/zones/disable?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/disable`\ `/api/disableZone` PERMISSIONS:\ Zones: Modify\ Zone: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to be disabled. RESPONSE: ``` { "status": "ok" } ``` ### Delete Zone Deletes an authoritative zone. URL:\ `http://localhost:5380/api/zones/delete?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/delete`\ `/api/deleteZone` PERMISSIONS:\ Zones: Delete\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to be deleted. RESPONSE: ``` { "status": "ok" } ``` ### Resync Zone Allows resyncing a Secondary or Stub zone. This process will re-fetch all the records from the primary name server for the zone. URL:\ `http://localhost:5380/api/zones/resync?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/resync` PERMISSIONS:\ Zones: Modify\ Zone: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to resync. RESPONSE: ``` { "status": "ok" } ``` ### Get Zone Options Gets the zone specific options. URL:\ `http://localhost:5380/api/zones/options/get?token=x&zone=example.com&includeAvailableTsigKeyNames=true` OBSOLETE PATH:\ `/api/zone/options` PERMISSIONS:\ Zones: Modify\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to get options. - `includeAvailableCatalogZoneNames`: Set to `true` to include list of available Catalog zone names on the DNS server. - `includeAvailableTsigKeyNames`: Set to `true` to include list of available TSIG key names on the DNS server. RESPONSE: ``` { "response": { "name": "example.com", "type": "Primary", "internal": false, "dnssecStatus": "Unsigned", "notifyFailed": true, "notifyFailedFor": [ "192.168.10.5" ], "disabled": false, "catalog": "catalog1", "overrideCatalogQueryAccess": false, "overrideCatalogZoneTransfer": false, "overrideCatalogNotify": false, "queryAccess": "Allow", "queryAccessNetworkACL": [], "zoneTransfer": "AllowOnlyZoneNameServers", "zoneTransferNetworkACL": [], "zoneTransferTsigKeyNames": [ "key.example.com" ], "notify": "ZoneNameServers", "notifyNameServers": [], "update": "UseSpecifiedNetworkACL", "updateNetworkACL": [ "192.168.180.0/24" ], "updateSecurityPolicies": [ { "tsigKeyName": "key.example.com", "domain": "example.com", "allowedTypes": [ "A", "AAAA" ] }, { "tsigKeyName": "key.example.com", "domain": "*.example.com", "allowedTypes": [ "ANY" ] } ], "availableCatalogZoneNames": [ "catalog1" ], "availableTsigKeyNames": [ "key.example.com", "catalog" ] }, "status": "ok" } ``` ### Set Zone Options Sets the zone specific options. URL:\ `http://localhost:5380/api/zones/options/set?token=x&zone=example.com&disabled=false&zoneTransfer=Allow&zoneTransferNameServers=¬ify=ZoneNameServers¬ifyNameServers=` OBSOLETE PATH:\ `/api/zone/options` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to set options. - `disabled` (optional): Sets if the zone is enabled or disabled. - `catalog` (optional): Set a Catalog zone name to register as its member zone. This option is valid only for `Primary`, `Secondary`, `Stub`, and `Forwarder` zones. - `overrideCatalogQueryAccess` (optional): Set to `true` to override Query Access option in the Catalog zone. This option is valid only for `Primary`, `Secondary`, `Stub`, and `Forwarder` zones. - `overrideCatalogZoneTransfer` (optional): Set to `true` to override Zone Transfer option in the Catalog zone. This option is valid only for `Primary`, `Secondary`, and `Forwarder` zones. - `overrideCatalogNotify` (optional): Set to `true` to override Notify option in the Catalog zone. This option is valid only for `Primary`, and `Forwarder` zones. - `primaryNameServerAddresses` (optional): List of comma separated IP addresses or domain names of the primary name server. This optional parameter is used only with `Secondary`, `SecondaryForwarder`, `SecondaryCatalog`, and `Stub` zones. If this parameter is not used, the DNS server will try to recursively resolve the primary name server addresses automatically for `Secondary` and `Stub` zones. This option is required for `SecondaryForwarder` and `SecondaryCatalog` zones. - `primaryZoneTransferProtocol `(optional): The zone transfer protocol to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones. Valid values are [`Tcp`, `Tls`, `Quic`]. - `primaryZoneTransferTsigKeyName` (optional): The TSIG key name to be used by `Secondary`, `SecondaryForwarder`, and `SecondaryCatalog` zones for zone transfer. - `validateZone`: (optional): Set value as `true` to enable ZONEMD validation. When enabled, the `Secondary` zone will be validated using the ZONEMD record after every zone transfer. The zone will get disabled if the validation fails. The zone must be DNSSEC signed for the validation to work. This option is only valid for `Secondary` zones. - `queryAccess` (optional): Valid options are [`Deny`, `Allow`, `AllowOnlyPrivateNetworks`, `AllowOnlyZoneNameServers`, `UseSpecifiedNetworkACL`, `AllowZoneNameServersAndUseSpecifiedNetworkACL`]. - `queryAccessNetworkACL` (optional): A comma separated Access Control List (ACL) of Network Access Control (NAC) entry. NAC is an IP address or network address to allow. Add `!` at the start of the NAC to deny access. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all except loopback. Set this parameter to `false` to remove existing values. This option is valid for all zones except `SecondaryCatalog` zone and only when `queryAccess` is set to `UseSpecifiedNetworkACL` or `AllowZoneNameServersAndUseSpecifiedNetworkACL`. - `zoneTransfer` (optional): Sets if the zone allows zone transfer. Valid options are [`Deny`, `Allow`, `AllowOnlyZoneNameServers`, `UseSpecifiedNetworkACL`, `AllowZoneNameServersAndUseSpecifiedNetworkACL`]. This option is valid only for Primary and Secondary zones. - `zoneTransferNetworkACL` (optional): A comma separated Access Control List (ACL) of Network Access Control (NAC) entry. NAC is an IP address or network address to allow. Add `!` at the start of the NAC to deny access. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all. Set this parameter to `false` to remove existing values. This option is valid only for `Primary`, `Secondary`, `Forwarder`, and `Catalog` zones and only when `zoneTransfer` is set to `UseSpecifiedNetworkACL` or `AllowZoneNameServersAndUseSpecifiedNetworkACL`. - `zoneTransferTsigKeyNames` (optional): A list of comma separated TSIG keys names that are authorized to perform a zone transfer. Set this option to `false` to clear all key names. This option is valid only for `Primary`, `Secondary`, `Forwarder`, and `Catalog` zones. - `notify` (optional): Sets if the DNS server should notify other DNS servers for zone updates. Valid options for `Primary` and `Secondary` zones are [`None`, `ZoneNameServers`, `SpecifiedNameServers`, `BothZoneAndSpecifiedNameServers`, `SeparateNameServersForCatalogAndMemberZones`]. Valid options for `Forwarder` and `Catalog` zones are [`None`, `SpecifiedNameServers`]. The `SeparateNameServersForCatalogAndMemberZones` option is valid only for `Catalog` zones. This option is valid only for `Primary`, `Secondary`, `Forwarder`, and `Catalog` zones. - `notifyNameServers` (optional): A list of comma separated IP addresses which should be notified by the DNS server for zone updates. This list is used only when `notify` option is set to `SpecifiedNameServers` or `BothZoneAndSpecifiedNameServers`. This option is valid only for `Primary`, `Secondary`, `Forwarder`, and `Catalog` zones. - `notifySecondaryCatalogsNameServers` (optional): A list of comma separated IP addresses which should be notified by the DNS server only for catalog zone updates. This list is used only when `notify` option is set to `SeparateNameServersForCatalogAndMemberZones`. This option is valid only for `Catalog` zones. - `update` (optional): Sets if the DNS server should allow dynamic updates (RFC 2136). This option is valid only for `Primary`, `Secondary`, and `Forwarder` zones. Valid options for `Primary` zones are [`Deny`, `Allow`, `AllowOnlyZoneNameServers`, `UseSpecifiedNetworkACL`, `AllowZoneNameServersAndUseSpecifiedNetworkACL`]. Valid options for `Secondary` and `Forwarder` zones are [`Deny`, `Allow`, `UseSpecifiedNetworkACL`]. - `updateNetworkACL` (optional): A comma separated Access Control List (ACL) of Network Access Control (NAC) entry. NAC is an IP address or network address to allow. Add `!` at the start of the NAC to deny access. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all. Set this parameter to `false` to remove existing values. This option is valid only for `Primary`, `Secondary`, and `Forwarder` zones and only when `update` is set to `UseSpecifiedNetworkACL` or `AllowZoneNameServersAndUseSpecifiedNetworkACL`. - `updateSecurityPolicies` (optional): A pipe `|` separated (used as both row and column separator) table data of security policies with each row containing the TSIG keys name, domain name and record types (comma separated) that are allowed. Use wildcard domain name to specify all sub domain names. Set this option to `false` to clear all security policies and stop TSIG authentication. This option is valid only for `Primary` and `Forwarder` zones. RESPONSE: ``` { "status": "ok" } ``` ### Get Zone Permissions Gets the zone specific permissions. URL:\ `http://localhost:5380/api/zones/permissions/get?token=x&zone=example.com&includeUsersAndGroups=true` PERMISSIONS:\ Zones: Modify\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to get the permissions for. - `includeUsersAndGroups`: Set to true to get a list of users and groups in the response. RESPONSE: ``` { "response": { "section": "Zones", "subItem": "example.com", "userPermissions": [ { "username": "admin", "canView": true, "canModify": true, "canDelete": true } ], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true } ], "users": [ "admin", "shreyas" ], "groups": [ "Administrators", "DHCP Administrators", "DNS Administrators", "Everyone" ] }, "status": "ok" } ``` ### Set Zone Permissions Sets the zone specific permissions. URL:\ `http://localhost:5380/api/zones/permissions/set?token=x&zone=example.com&userPermissions=admin|true|true|true&groupPermissions=Administrators|true|true|true|DNS%20Administrators|true|true|true` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The domain name of the zone to get the permissions for. - `userPermissions` (optional): A pipe `|` separated table data with each row containing username and boolean values for the view, modify and delete permissions. For example: user1|true|true|true|user2|true|false|false - `groupPermissions` (optional): A pipe `|` separated table data with each row containing the group name and boolean values for the view, modify and delete permissions. For example: group1|true|true|true|group2|true|true|false RESPONSE: ``` { "response": { "section": "Zones", "subItem": "example.com", "userPermissions": [ { "username": "admin", "canView": true, "canModify": true, "canDelete": true } ], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true } ] }, "status": "ok" } ``` ### Sign Zone Signs the primary zone (DNSSEC). URL:\ `http://localhost:5380/api/zones/dnssec/sign?token=x&zone=example.com&algorithm=ECDSA&dnsKeyTtl=86400&zskRolloverDays=30&nxProof=NSEC3&iterations=0&saltLength=0&curve=P256` OBSOLETE PATH:\ `/api/zone/dnssec/sign` PERMISSONS: Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone to sign. - `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`]. - `pemKskPrivateKey` (optional): The user specified private key in PEM format for Key Signing Key (KSK). When this parameter is specified, the private key specified is used instead of automatically generating it. - `pemZskPrivateKey` (optional): The user specified private key in PEM format for Zone Signing Key (ZSK). When this parameter is specified, the private key specified is used instead of automatically generating it. - `hashAlgorithm` (optional): The hash algorithm to be used when using `RSA` algorithm. Valid values are [`MD5`, `SHA1`, `SHA256`, `SHA512`]. This optional parameter is required when using `RSA` algorithm. - `kskKeySize` (optional): The size of the Key Signing Key (KSK) in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm. - `zskKeySize` (optional): The size of the Zone Signing Key (ZSK) in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm. - `curve` (optional): The name of the curve to be used when using `ECDSA` or `EDDSA` algorithm. Valid values are [`P256`, `P384`] for `ECDSA` algorithm and [`ED25519`, `ED448`] for `EDDSA` algorithm. This optional parameter is required when using `ECDSA` or `EDDSA` algorithm. - `dnsKeyTtl` (optional): The TTL value to be used for DNSKEY records. Default value is `86400` when not specified. - `zskRolloverDays` (optional): The frequency in days that the DNS server must automatically rollover the Zone Signing Keys (ZSK) in the zone. Valid range is 0-365 days where 0 disables rollover. Default value is `30` when not specified. - `nxProof` (optional): The type of proof of non-existence that must be used for signing the zone. Valid values are [`NSEC`, `NSEC3`]. Default value is `NSEC` when not specified. - `iterations` (optional): The number of iterations to use for hashing in NSEC3. This optional parameter is only applicable when using `NSEC3` as the `nxProof`. Default value is `0` when not specified. - `saltLength` (optional): The length of salt in bytes to use for hashing in NSEC3. This optional parameter is only applicable when using `NSEC3` as the `nxProof`. Default value is `0` when not specified. RESPONSE: ``` { "status": "ok" } ``` ### Unsign Zone Unsigns the primary zone (DNSSEC). URL:\ `http://localhost:5380/api/zones/dnssec/unsign?token=x&zone=example.com OBSOLETE PATH:\ `/api/zone/dnssec/unsign` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone to unsign. RESPONSE: ``` { "status": "ok" } ``` ### Get DS Info Get the DS info for the signed primary zone to help with updating DS records at the parent zone. URL:\ `http://localhost:5380/api/zones/dnssec/viewDS?token=x&zone=example.com PERMISSIONS:\ Zones: View\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the signed primary zone. RESPONSE: ``` { "response": { "name": "example.com", "type": "Primary", "internal": false, "disabled": false, "dnssecStatus": "SignedWithNSEC", "dsRecords": [ { "keyTag": 47972, "dnsKeyState": "Published", "dnsKeyStateReadyBy": "2023-10-29T16:20:08.8007369Z", "algorithm": "ECDSAP256SHA256", "publicKey": "TK5a8pXPMspDwuh4Z3evOfNZm9kkc8IzwZDiCgIX6imxwkbpY9FTvhoI/ttZiLWZ5hvLbvrpsbd0liqSwqNmPg==", "digests": [ { "digestType": "SHA256", "digest": "D59EBB413C88576B519B2980DF50493689A4A260383D0CB2F260251D5CA2E144" }, { "digestType": "SHA384", "digest": "F8235EEAB1AEBCFAD28096DF8DCF820F25C685041562AAB63E1A3E1AC89D2FC3836E97114A64EC0E057DCA234451E50C" } ] } ] }, "status": "ok" } ``` ### Get DNSSEC Properties Get the DNSSEC properties for the primary zone. URL:\ `http://localhost:5380/api/zones/dnssec/properties/get?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/dnssec/getProperties` PERMISSIONS:\ Zones: Modify\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. RESPONSE: ``` { "response": { "name": "example.com", "type": "Primary", "internal": false, "disabled": false, "dnssecStatus": "SignedWithNSEC", "dnsKeyTtl": 3600, "dnssecPrivateKeys": [ { "keyTag": 15048, "keyType": "KeySigningKey", "algorithm": "ECDSAP256SHA256", "state": "Published", "stateChangedOn": "2022-12-18T14:39:50.0328321Z", "stateReadyBy": "2022-12-18T16:14:50.0328321Z", "isRetiring": false, "rolloverDays": 0 }, { "keyTag": 46152, "keyType": "ZoneSigningKey", "algorithm": "ECDSAP256SHA256", "state": "Active", "stateChangedOn": "2022-12-18T14:39:50.0661173Z", "isRetiring": false, "rolloverDays": 90 } ] }, "status": "ok" } ``` ### Convert To NSEC Converts a primary zone from NSEC3 to NSEC for proof of non-existence. URL:\ `http://localhost:5380/api/zones/dnssec/properties/convertToNSEC?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/dnssec/convertToNSEC` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. RESPONSE: ``` { "status": "ok" } ``` ### Convert To NSEC3 Converts a primary zone from NSEC to NSEC3 for proof of non-existence. URL:\ `http://localhost:5380/api/zones/dnssec/properties/convertToNSEC3?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/dnssec/convertToNSEC3` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. RESPONSE: ``` { "status": "ok" } ``` ### Update NSEC3 Parameters Updates the iteration and salt length parameters for NSEC3. URL:\ `http://localhost:5380/api/zones/dnssec/properties/updateNSEC3Params?token=x&zone=example.com&iterations=0&saltLength=0` OBSOLETE PATH:\ `/api/zone/dnssec/updateNSEC3Params` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `iterations` (optional): The number of iterations to use for hashing. Default value is `0` when not specified. - `saltLength` (optional): The length of salt in bytes to use for hashing. Default value is `0` when not specified. RESPONSE: ``` { "status": "ok" } ``` ### Update DNSKEY TTL Updates the TTL value for DNSKEY resource record set. The value can be updated only when all the DNSKEYs are in ready or active state. URL:\ `http://localhost:5380/api/zones/dnssec/properties/updateDnsKeyTtl?token=x&zone=example.com&ttl=86400` OBSOLETE PATH:\ `/api/zone/dnssec/updateDnsKeyTtl` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `ttl`: The TTL value for the DNSKEY resource record set. RESPONSE: ``` { "status": "ok" } ``` ### Add Private Key Adds a private key to be used for signing the zone with DNSSEC. URL:\ `http://localhost:5380/api/zones/dnssec/properties/addPrivateKey?token=x&zone=example.com&keyType=KeySigningKey&algorithm=ECDSA&curve=P256` OBSOLETE PATH:\ `/api/zone/dnssec/generatePrivateKey`\ `/api/zones/dnssec/properties/generatePrivateKey` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `keyType`: The type of key for which the private key is to be generated. Valid values are [`KeySigningKey`, `ZoneSigningKey`]. - `rolloverDays` (optional): The frequency in days that the DNS server must automatically rollover the private key in the zone. Valid range is 0-365 days where 0 disables rollover. Default value is 90 days for Zone Signing Key (ZSK) and 0 days for Key Signing Key (KSK). - `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`]. - `pemPrivateKey` (optional): Specifies a user generated private key in PEM format to add. When not specified a private key will be automatically generated. - `hashAlgorithm` (optional): The hash algorithm to be used when using `RSA` algorithm. Valid values are [`MD5`, `SHA1`, `SHA256`, `SHA512`]. This optional parameter is required when using `RSA` algorithm. - `keySize` (optional): The size of the generated private key in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm. - `curve` (optional): The name of the curve to be used when using `ECDSA` or `EDDSA` algorithm. Valid values are [`P256`, `P384`] for `ECDSA` algorithm and [`ED25519`, `ED448`] for `EDDSA` algorithm. This optional parameter is required when using `ECDSA` or `EDDSA` algorithm. RESPONSE: ``` { "status": "ok" } ``` ### Update Private Key Updates the DNSSEC private key properties. URL:\ `http://localhost:5380/api/zones/dnssec/properties/updatePrivateKey?token=x&zone=example.com&keyTag=1234&rolloverDays=90` OBSOLETE PATH:\ `/api/zone/dnssec/updatePrivateKey` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `keyTag`: The key tag of the private key to be updated. - `rolloverDays`: The frequency in days that the DNS server must automatically rollover the private key in the zone. Valid range is 0-365 days where 0 disables rollover. RESPONSE: ``` { "status": "ok" } ``` ### Delete Private Key Deletes a private key that has state set as `Generated`. Private keys with any other state cannot be delete. URL:\ `http://localhost:5380/api/zones/dnssec/properties/deletePrivateKey?token=x&zone=example.com&keyTag=12345` OBSOLETE PATH:\ `/api/zone/dnssec/deletePrivateKey` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `keyTag`: The key tag of the private key to be deleted. RESPONSE: ``` { "status": "ok" } ``` ### Publish All Private Keys Publishes all private keys that have state set as `Generated` by adding associated DNSKEY records for them. Once published, the keys will be automatically activated. For Key Signing Keys (KSK), once the state is set to `Ready` you can then safely replace the old DS record from the parent zone with a new DS key record for the KSK associated DNSKEY record. Once the new DS record is published at the parent zone, the DNS server will automatically detect and set the KSK state to `Active`. URL:\ `http://localhost:5380/api/zones/dnssec/properties/publishAllPrivateKeys?token=x&zone=example.com` OBSOLETE PATH:\ `/api/zone/dnssec/publishAllPrivateKeys` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. RESPONSE: ``` { "status": "ok" } ``` ### Rollover DNSKEY Generates and publishes a new private key for the given key that has to be rolled over. The old private key and its associated DNSKEY record will be automatically retired and removed safely once the new key is active. URL:\ `http://localhost:5380/api/zones/dnssec/properties/rolloverDnsKey?token=x&zone=example.com&keyTag=12345` OBSOLETE PATH:\ `/api/zone/dnssec/rolloverDnsKey` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `keyTag`: The key tag of the private key to rollover. RESPONSE: ``` { "status": "ok" } ``` ### Retire DNSKEY Retires the specified private key and its associated DNSKEY record and removes it safely. To retire an existing DNSKEY, there must be at least one active key available. URL:\ `http://localhost:5380/api/zones/dnssec/properties/retireDnsKey?token=x&zone=example.com&keyTag=12345` OBSOLETE PATH:\ `/api/zone/dnssec/retireDnsKey` PERMISSIONS:\ Zones: Modify\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `zone`: The name of the primary zone. - `keyTag`: The key tag of the private key to retire. RESPONSE: ``` { "status": "ok" } ``` ### Add Record Adds an resource record for an authoritative zone. URL:\ `http://localhost:5380/api/zones/records/add?token=x&domain=example.com&zone=example.com` OBSOLETE PATH:\ `/api/zone/addRecord`\ `/api/addRecord` PERMISSIONS:\ Zones: None\ Zone: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `domain`: The domain name of the zone to add record. - `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used. - `type`: The DNS resource record type. Supported record types are [`A`, `AAAA`, `NS`, `CNAME`, `PTR`, `MX`, `TXT`, `SRV`, `DNAME`, `DS`, `SSHFP`, `TLSA`, `SVCB`, `HTTPS`, `URI`, `CAA`] and proprietary types [`ANAME`, `FWD`, `APP`]. Unknown record types are also supported since v11.2. - `ttl` (optional): The DNS resource record TTL value. This is the value in seconds that the DNS resolvers can cache the record for. When not specified the default TTL value from settings will be used. - `overwrite` (optional): This option when set to `true` will overwrite existing resource record set for the selected `type` with the new record. Default value of `false` will add the new record into existing resource record set. - `comments` (optional): Sets comments for the added resource record. - `expiryTtl` (optional): Set to automatically delete the record when the value in seconds elapses since the record’s last modified time. - `ipAddress` (optional): The IP address for adding `A` or `AAAA` record. A special value of `request-ip-address` can be used to set the record with the IP address of the API HTTP request to help with dynamic DNS update applications. This option is required and used only for `A` and `AAAA` records. - `ptr` (optional): Set this option to `true` to add a reverse PTR record for the IP address in the `A` or `AAAA` record. This option is used only for `A` and `AAAA` records. - `createPtrZone` (optional): Set this option to `true` to create a reverse zone for PTR record. This option is used for `A` and `AAAA` records. - `updateSvcbHints` (optional): Set this option to `true` to update any SVCB/HTTPS records in the zone that has Automatic Hints option enabled and matches its target name with the current record's domain name. This option is used for `A` and `AAAA` records. - `nameServer` (optional): The name server domain name. This option is required for adding `NS` record. - `glue` (optional): This is the glue address for the name server in the `NS` record. This optional parameter is used for adding `NS` record. - `cname` (optional): The CNAME domain name. This option is required for adding `CNAME` record. - `ptrName` (optional): The PTR domain name. This option is required for adding `PTR` record. - `exchange` (optional): The exchange domain name. This option is required for adding `MX` record. - `preference` (optional): This is the preference value for `MX` record type. This option is required for adding `MX` record. - `text` (optional): The text data for `TXT` record. This option is required for adding `TXT` record. - `splitText` (optional): Set to `true` for using new line char to split text into multiple character-strings for adding `TXT` record. - `mailbox` (optional): Set an email address for adding `RP` record. - `txtDomain` (optional): Set a `TXT` record's domain name for adding `RP` record. - `priority` (optional): This parameter is required for adding the `SRV` record. - `weight` (optional): This parameter is required for adding the `SRV` record. - `port` (optional): This parameter is required for adding the `SRV` record. - `target` (optional): This parameter is required for adding the `SRV` record. - `naptrOrder` (optional): This parameter is required for adding the `NAPTR` record. - `naptrPreference` (optional): This parameter is required for adding the `NAPTR` record. - `naptrFlags` (optional): This parameter is required for adding the `NAPTR` record. - `naptrServices` (optional): This parameter is required for adding the `NAPTR` record. - `naptrRegexp` (optional): This parameter is required for adding the `NAPTR` record. - `naptrReplacement` (optional): This parameter is required for adding the `NAPTR` record. - `dname` (optional): The DNAME domain name. This option is required for adding `DNAME` record. - `keyTag` (optional): This parameter is required for adding `DS` record. - `algorithm` (optional): Valid values are [`RSAMD5`, `DSA`, `RSASHA1`, `DSA-NSEC3-SHA1`, `RSASHA1-NSEC3-SHA1`, `RSASHA256`, `RSASHA512`, `ECC-GOST`, `ECDSAP256SHA256`, `ECDSAP384SHA384`, `ED25519`, `ED448`]. This parameter is required for adding `DS` record. - `digestType` (optional): Valid values are [`SHA1`, `SHA256`, `GOST-R-34-11-94`, `SHA384`]. This parameter is required for adding `DS` record. - `digest` (optional): A hex string value. This parameter is required for adding `DS` record. - `sshfpAlgorithm` (optional): Valid values are [`RSA`, `DSA`, `ECDSA`, `Ed25519`, `Ed448`]. This parameter is required for adding `SSHFP` record. - `sshfpFingerprintType` (optional): Valid values are [`SHA1`, `SHA256`]. This parameter is required for adding `SSHFP` record. - `sshfpFingerprint` (optional): A hex string value. This parameter is required for adding `SSHFP` record. - `tlsaCertificateUsage` (optional): Valid values are [`PKIX-TA`, `PKIX-EE`, `DANE-TA`, `DANE-EE`]. This parameter is required for adding `TLSA` record. - `tlsaSelector` (optional): Valid values are [`Cert`, `SPKI`]. This parameter is required for adding `TLSA` record. - `tlsaMatchingType` (optional): Valid value are [`Full`, `SHA2-256`, `SHA2-512`]. This parameter is required for adding `TLSA` record. - `tlsaCertificateAssociationData` (optional): A X509 certificate in PEM format or a hex string value. This parameter is required for adding `TLSA` record. - `svcPriority` (optional): The priority value for `SVCB` or `HTTPS` record. This parameter is required for adding `SCVB` or `HTTPS` record. - `svcTargetName` (optional): The target domain name for `SVCB` or `HTTPS` record. This parameter is required for adding `SCVB` or `HTTPS` record. - `svcParams` (optional): The service parameters for `SVCB` or `HTTPS` record which is a pipe separated list of key and value. For example, `alpn|h2,h3|port|53443`. To clear existing values, set it to `false`. This parameter is required for adding `SCVB` or `HTTPS` record. - `autoIpv4Hint` (optional): Set this option to `true` to enable Automatic Hints for the `ipv4hint` parameter in the `svcParams`. This option is valid only for `SVCB` and `HTTPS` records. - `autoIpv6Hint` (optional): Set this option to `true` to enable Automatic Hints for the `ipv6hint` parameter in the `svcParams`. This option is valid only for `SVCB` and `HTTPS` records. - `uriPriority` (optional): The priority value for adding the `URI` record. - `uriWeight` (optional): The weight value for adding the `URI` record. - `uri` (optional): The URI value for adding the `URI` record. - `flags` (optional): This parameter is required for adding the `CAA` record. - `tag` (optional): This parameter is required for adding the `CAA` record. - `value` (optional): This parameter is required for adding the `CAA` record. - `aname` (optional): The ANAME domain name. This option is required for adding `ANAME` record. - `protocol` (optional): This parameter is required for adding the `FWD` record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. - `forwarder` (optional): The forwarder address. A special value of `this-server` can be used to directly forward requests internally to the DNS server. This parameter is required for adding the `FWD` record. - `forwarderPriority` (optional): Set an integer priority value for adding the `FWD` record. Forwarders with high priority (lower value) will be queried before trying for low priority forwarders. Forwarders with the same priority will be concurrently queried. - `dnssecValidation` (optional): Set this boolean value to indicate if DNSSEC validation must be done. This optional parameter is to be used with FWD records. Default value is `false`. - `proxyType` (optional): The type of proxy that must be used for conditional forwarding. This optional parameter is to be used with FWD records. Valid values are [`NoProxy`, `DefaultProxy`, `Http`, `Socks5`]. Default value `DefaultProxy` is used when this parameter is missing. - `proxyAddress` (optional): The proxy server address to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `proxyPort` (optional): The proxy server port to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `proxyUsername` (optional): The proxy server username to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `proxyPassword` (optional): The proxy server password to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `appName` (optional): The name of the DNS app. This parameter is required for adding the `APP` record. - `classPath` (optional): This parameter is required for adding the `APP` record. - `recordData` (optional): This parameter is used for adding the `APP` record as per the DNS app requirements. - `rdata` (optional): This parameter is used for adding unknown i.e. unsupported record types. The value must be formatted as a hex string or a colon separated hex string. RESPONSE: ``` { "response": { "zone": { "name": "example.com", "type": "Primary", "internal": false, "dnssecStatus": "SignedWithNSEC", "disabled": false }, "addedRecord": { "disabled": false, "name": "example.com", "type": "A", "ttl": 3600, "rData": { "ipAddress": "3.3.3.3" }, "dnssecStatus": "Unknown", "lastUsedOn": "0001-01-01T00:00:00" } }, "status": "ok" } ``` ### Get Records Gets all records for a given authoritative zone. URL:\ `http://localhost:5380/api/zones/records/get?token=x&domain=example.com&zone=example.com&listZone=true` OBSOLETE PATH:\ `/api/zone/getRecords`\ `/api/getRecords` PERMISSIONS:\ Zones: None\ Zone: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `domain`: The domain name of the zone to get records. - `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used. - `listZone` (optional): When set to `true` will list all records in the zone else will list records only for the given domain name. Default value is `false` when not specified. RESPONSE: ``` { "response": { "zone": { "name": "example.com", "type": "Primary", "internal": false, "dnssecStatus": "SignedWithNSEC3", "disabled": false }, "records": [ { "disabled": false, "name": "example.com", "type": "A", "ttl": 3600, "rData": { "ipAddress": "1.1.1.1" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "NS", "ttl": 3600, "rData": { "nameServer": "server1" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "SOA", "ttl": 900, "rData": { "primaryNameServer": "server1", "responsiblePerson": "hostadmin.example.com", "serial": 35, "refresh": 900, "retry": 300, "expire": 604800, "minimum": 900 }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "RRSIG", "ttl": 900, "rData": { "typeCovered": "NSEC3PARAM", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 900, "signatureExpiration": "2022-03-15T11:45:31Z", "signatureInception": "2022-03-05T10:45:31Z", "keyTag": 61009, "signersName": "example.com", "signature": "vJ/fXkGKsapdvWjDhcfHsBxpZhSzMRLZv3/bEGJ4N3/K7jiM92Ik336W680SI7g+NyPCQ3gqE7ta/JEL4bht4Q==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "RRSIG", "ttl": 900, "rData": { "typeCovered": "SOA", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 900, "signatureExpiration": "2022-03-15T12:53:39Z", "signatureInception": "2022-03-05T11:53:39Z", "keyTag": 61009, "signersName": "example.com", "signature": "9PQHH3ZGCuFRYkn28SoilS8y8zszgeOpCfJpIOAaE5ao+iBPCXudHacr/EpgB2wLzXpRjR+WgiYjmJH17+6bKg==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "RRSIG", "ttl": 3600, "rData": { "typeCovered": "A", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 3600, "signatureExpiration": "2022-03-15T11:25:35Z", "signatureInception": "2022-03-05T10:25:35Z", "keyTag": 61009, "signersName": "example.com", "signature": "dWjn5hTWuEq57ncwGdVq+kdbMuFtuxLuZhYCcQMdsTxYkM/64RrPY6eYwfYQ7+fY1+QBSX2WudAM4dzbmL/s2A==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "RRSIG", "ttl": 3600, "rData": { "typeCovered": "NS", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 3600, "signatureExpiration": "2022-03-15T11:25:35Z", "signatureInception": "2022-03-05T10:25:35Z", "keyTag": 61009, "signersName": "example.com", "signature": "Yx+leBcYNFf0gUfN6rECWrUZwCDhJbAGk1BNOJN01nPakS5meSbDApUHJZeAzfSBcPzodK3ddmEuhho1MABaZw==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "RRSIG", "ttl": 86400, "rData": { "typeCovered": "DNSKEY", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 86400, "signatureExpiration": "2022-03-15T12:27:09Z", "signatureInception": "2022-03-05T11:27:09Z", "keyTag": 65078, "signersName": "example.com", "signature": "KWAK7o+FjJ2/6ZvX4C1wB41yRzlmec5pR2TTeNWlY/weg0MNKCLRs3uTopSjoTih+uq3IRR7Zx0iOcy7evOitA==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "RRSIG", "ttl": 86400, "rData": { "typeCovered": "DNSKEY", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 86400, "signatureExpiration": "2022-03-15T12:27:09Z", "signatureInception": "2022-03-05T11:27:09Z", "keyTag": 52896, "signersName": "example.com", "signature": "oHtt1gUmDXxI5GMfS+LJ6uxKUcuUu+5EELXdhLrbk5V/yganP6sMgA4hGkzokYM22LDowjSdO5qwzCW6IDgKxg==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "DNSKEY", "ttl": 86400, "rData": { "flags": "SecureEntryPoint, ZoneKey", "protocol": 3, "algorithm": "ECDSAP256SHA256", "publicKey": "dMRyc/Pji31mF3iHNrybPzbgvtb2NKtmXhjQq433BHI= ZveDa1z00VxDnugV1x7EDvpt+42TDh8OQwp1kOrpX0E=", "computedKeyTag": 65078, "dnsKeyState": "Ready", "computedDigests": [ { "digestType": "SHA256", "digest": "BBE017B17E5CB5FFFF1EC2C7815367DF80D8E7EAEE4832D3ED192159D79B1EEB" }, { "digestType": "SHA384", "digest": "0B0C9F1019BD3FE62C8B71F8C80E7A833BA468A7E303ABC819C0CB9BEDE8E26BB50CB1729547BFCCE2AE22390E44CDA3" } ] }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "DNSKEY", "ttl": 86400, "rData": { "flags": "ZoneKey", "protocol": 3, "algorithm": "ECDSAP256SHA256", "publicKey": "IUvzTkf4JPg+7k57cQw7n7SR6/1dH7FaKxu9Cf+kcvo= UU+uoKRWnYAFHDNF0X3U8ZYetUyDF7fcNAwEaSQnIUM=", "computedKeyTag": 61009, "dnsKeyState": "Active" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "DNSKEY", "ttl": 3600, "rData": { "flags": "SecureEntryPoint, ZoneKey", "protocol": 3, "algorithm": "ECDSAP256SHA256", "publicKey": "KOJWFitKm58EgjO43GDnsFbnkGoqVKeLRkP8FGPAdhqA2F758Ta1mkxieEu0YN0EoX+u5bVuc5DEBFSv+U63CA==", "computedKeyTag": 15048, "dnsKeyState": "Published", "dnsKeyStateReadyBy": "2022-12-18T16:14:50.0328321Z", "computedDigests": [ { "digestType": "SHA256", "digest": "8EAFAE3305DB57A27CA5A261525515461CB7232A34A44AD96441B88BCA9B9849" }, { "digestType": "SHA384", "digest": "4A6DA59E91872B5B835FCEE5987B17151A6F10FE409B595BEEEDB28FE64315C9C268493B59A0BF72EA84BE0F20A33F96" } ] }, "dnssecStatus": "Unknown", "lastUsedOn": "0001-01-01T00:00:00" }, { "disabled": false, "name": "example.com", "type": "DNSKEY", "ttl": 86400, "rData": { "flags": "ZoneKey", "protocol": 3, "algorithm": "ECDSAP256SHA256", "publicKey": "337uQ11fdKbr6sKYq9mwwBC2xdnu0geuIkfHcIauKNI= rKk7pfVKlLfcGBOIn5hEVeod2aIRIyUiivdTPzrmpIo=", "computedKeyTag": 4811, "dnsKeyState": "Published" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "example.com", "type": "NSEC3PARAM", "ttl": 900, "rData": { "hashAlgorithm": "SHA1", "flags": "None", "iterations": 0, "salt": "" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "*.example.com", "type": "A", "ttl": 3600, "rData": { "ipAddress": "7.7.7.7" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "*.example.com", "type": "RRSIG", "ttl": 3600, "rData": { "typeCovered": "A", "algorithm": "ECDSAP256SHA256", "labels": 2, "originalTtl": 3600, "signatureExpiration": "2022-03-15T11:25:35Z", "signatureInception": "2022-03-05T10:25:35Z", "keyTag": 61009, "signersName": "example.com", "signature": "ZoUNNEdb8XWqHHi5o4BcUe7deRVlJZLhQtc3sjRtuJ68DNPDmQ0GfCrNTigJcomspr7CYqWcXfoSOqu6f2AyyQ==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "4F3CNT8CU22TNGEC382JJ4GDE4RB47UB.example.com", "type": "RRSIG", "ttl": 900, "rData": { "typeCovered": "NSEC3", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 900, "signatureExpiration": "2022-03-15T11:45:31Z", "signatureInception": "2022-03-05T10:45:31Z", "keyTag": 61009, "signersName": "example.com", "signature": "piZeLYa6WpHyiJerPlXq2s+JKBjHznNALXHJCOfiQ4o/iTqWILoqYHfKB5AWrLwLmkxXcbKf63CnEMGlinRidg==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "4F3CNT8CU22TNGEC382JJ4GDE4RB47UB.example.com", "type": "NSEC3", "ttl": 900, "rData": { "hashAlgorithm": "SHA1", "flags": "None", "iterations": 0, "salt": "", "nextHashedOwnerName": "KG19N32806C832KIJDNGLQ8P9M2R5MDJ", "types": [ "A" ] }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "KG19N32806C832KIJDNGLQ8P9M2R5MDJ.example.com", "type": "RRSIG", "ttl": 900, "rData": { "typeCovered": "NSEC3", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 900, "signatureExpiration": "2022-03-15T11:45:31Z", "signatureInception": "2022-03-05T10:45:31Z", "keyTag": 61009, "signersName": "example.com", "signature": "i/PMxc1LFA9a8jLxju7SSpoY7y8aZYkAILcCRIxE3lTundPJmzFG0U9kve04kqT7+Klmzj3OzXnCvjTA54+DZA==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "KG19N32806C832KIJDNGLQ8P9M2R5MDJ.example.com", "type": "NSEC3", "ttl": 900, "rData": { "hashAlgorithm": "SHA1", "flags": "None", "iterations": 0, "salt": "", "nextHashedOwnerName": "MIFDNDT3NFF3OD53O7TLA1HRFF95JKUK", "types": [ "NS", "DS" ] }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "MIFDNDT3NFF3OD53O7TLA1HRFF95JKUK.example.com", "type": "RRSIG", "ttl": 900, "rData": { "typeCovered": "NSEC3", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 900, "signatureExpiration": "2022-03-15T11:45:31Z", "signatureInception": "2022-03-05T10:45:31Z", "keyTag": 61009, "signersName": "example.com", "signature": "mr37TDMmWJ3YLNtpYy++S9eAeHIXKajX6jB8zLscJyC1uI0OFnSTuesfhIlLDbj0SDgrzRQWsLmvMKzfq89TJA==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "MIFDNDT3NFF3OD53O7TLA1HRFF95JKUK.example.com", "type": "NSEC3", "ttl": 900, "rData": { "hashAlgorithm": "SHA1", "flags": "None", "iterations": 0, "salt": "", "nextHashedOwnerName": "ONIB9MGUB9H0RML3CDF5BGRJ59DKJHVK", "types": [ "CNAME" ] }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "ONIB9MGUB9H0RML3CDF5BGRJ59DKJHVK.example.com", "type": "RRSIG", "ttl": 900, "rData": { "typeCovered": "NSEC3", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 900, "signatureExpiration": "2022-03-15T11:45:31Z", "signatureInception": "2022-03-05T10:45:31Z", "keyTag": 61009, "signersName": "example.com", "signature": "GGh/KkB6C2D55xRJa0zFbZ8As3DZK9btUamryZVmyo7FaLPyltkeRZor9OExgQ6HC1SLXNGJIfCO9cM4K6P8iw==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "ONIB9MGUB9H0RML3CDF5BGRJ59DKJHVK.example.com", "type": "NSEC3", "ttl": 900, "rData": { "hashAlgorithm": "SHA1", "flags": "None", "iterations": 0, "salt": "", "nextHashedOwnerName": "4F3CNT8CU22TNGEC382JJ4GDE4RB47UB", "types": [ "A", "NS", "SOA", "DNSKEY", "NSEC3PARAM" ] }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "sub.example.com", "type": "NS", "ttl": 3600, "rData": { "nameServer": "server1" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "sub.example.com", "type": "DS", "ttl": 3600, "rData": { "keyTag": 46125, "algorithm": "ECDSAP384SHA384", "digestType": "SHA1", "digest": "5590E425472785A16DC0F853000557DB5543C39E" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "sub.example.com", "type": "RRSIG", "ttl": 3600, "rData": { "typeCovered": "NS", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 3600, "signatureExpiration": "2022-03-15T11:25:35Z", "signatureInception": "2022-03-05T10:25:35Z", "keyTag": 61009, "signersName": "example.com", "signature": "hFzYTL9V0/0UQZlvZpRWCOvu/2udvhswKoxpe4+quNuC6K59W7uCJLuDm/z0aFK5nW8Of4oTk2YjSBZo0nBSlg==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "sub.example.com", "type": "RRSIG", "ttl": 3600, "rData": { "typeCovered": "DS", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 3600, "signatureExpiration": "2022-03-15T12:53:39Z", "signatureInception": "2022-03-05T11:53:39Z", "keyTag": 61009, "signersName": "example.com", "signature": "UYpUKV5Uq7DM3rltg3sPFOwYgRa2yBzT/j9U8xCh5oyXt27fIn3eemvqqe9qV4xeQaAN0QfQPkj9vmOZSAYafg==" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "www.example.com", "type": "CNAME", "ttl": 3600, "rData": { "cname": "example.com" }, "dnssecStatus": "Unknown" }, { "disabled": false, "name": "www.example.com", "type": "RRSIG", "ttl": 3600, "rData": { "typeCovered": "CNAME", "algorithm": "ECDSAP256SHA256", "labels": 3, "originalTtl": 3600, "signatureExpiration": "2022-03-15T11:25:35Z", "signatureInception": "2022-03-05T10:25:35Z", "keyTag": 61009, "signersName": "example.com", "signature": "cAbYvDJhZGLS/uI5I4mSrh7S5gEUy6bmX2sY7zEd1XVFPqrUOZHbVZuwXPjA6r9/m0rCaww9RiG90JhNNDLEtA==" }, "dnssecStatus": "Unknown" } ] }, "status": "ok" } ``` ### Update Record Updates an existing record in an authoritative zone. URL:\ `http://localhost:5380/api/zones/records/update?token=x&domain=mail.example.com&zone=example.com&type=A&value=127.0.0.1&newValue=127.0.0.2&ptr=false` OBSOLETE PATH:\ `/api/zone/updateRecord`\ `/api/updateRecord` PERMISSIONS:\ Zones: None\ Zone: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `domain`: The domain name of the zone to update the record. - `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used. - `type`: The type of the resource record to update. - `newDomain` (optional): The new domain name to be set for the record. To be used to rename sub domain name of the record. - `ttl` (optional): The TTL value of the resource record. Default value of `3600` is used when parameter is missing. - `disable` (optional): Specifies if the record should be disabled. The default value is `false` when this parameter is missing. - `comments` (optional): Sets comments for the updated resource record. - `expiryTtl` (optional): Set to automatically delete the record when the value in seconds elapses since the record’s last modified time. - `ipAddress` (optional): The current IP address in the `A` or `AAAA` record. This parameter is required when updating `A` or `AAAA` record. - `newIpAddress` (optional): The new IP address in the `A` or `AAAA` record. This parameter when missing will use the current value in the record. - `ptr` (optional): Set this option to `true` to specify if the PTR record associated with the `A` or `AAAA` record must also be updated. This option is used only for `A` and `AAAA` records. - `createPtrZone` (optional): Set this option to `true` to create a reverse zone for PTR record. This option is used only for `A` and `AAAA` records. - `updateSvcbHints` (optional): Set this option to `true` to update any SVCB/HTTPS records in the zone that has Automatic Hints option enabled and matches its target name with the current record's domain name. This option is used for `A` and `AAAA` records. - `nameServer` (optional): The current name server domain name. This option is required for updating `NS` record. - `newNameServer` (optional): The new server domain name. This option is used for updating `NS` record. - `glue` (optional): The comma separated list of IP addresses set as glue for the NS record. This parameter is used only when updating `NS` record. - `cname` (optional): The CNAME domain name to update in the existing `CNAME` record. - `primaryNameServer` (optional): This is the primary name server parameter in the SOA record. This parameter is required when updating the SOA record. - `responsiblePerson` (optional): This is the responsible person parameter in the SOA record. This parameter is required when updating the SOA record. - `serial` (optional): This is the serial parameter in the SOA record. This parameter is required when updating the SOA record. - `refresh` (optional): This is the refresh parameter in the SOA record. This parameter is required when updating the SOA record. - `retry` (optional): This is the retry parameter in the SOA record. This parameter is required when updating the SOA record. - `expire` (optional): This is the expire parameter in the SOA record. This parameter is required when updating the SOA record. - `minimum` (optional): This is the minimum parameter in the SOA record. This parameter is required when updating the SOA record. - `useSerialDateScheme` (optional): Set value to `true` to enable using date scheme for SOA serial. This optional parameter is used only with `Primary`, `Forwarder`, and `Catalog` zones. Default value is `false`. This parameter is required when updating the SOA record. - `ptrName`(optional): The current PTR domain name. This option is required for updating `PTR` record. - `newPtrName`(optional): The new PTR domain name. This option is required for updating `PTR` record. - `preference` (optional): The current preference value in an MX record. This parameter when missing will default to `1` value. This parameter is used only when updating `MX` record. - `newPreference` (optional): The new preference value in an MX record. This parameter when missing will use the old value. This parameter is used only when updating `MX` record. - `exchange` (optional): The current exchange domain name. This option is required for updating `MX` record. - `newExchange` (optional): The new exchange domain name. This option is required for updating `MX` record. - `text` (optional): The current text value. This option is required for updating `TXT` record. - `newText` (optional): The new text value. This option is required for updating `TXT` record. - `splitText` (optional): The current split text value. This option is used for updating `TXT` record and is set to `false` when unspecified. - `newSplitText` (optional): The new split text value. This option is used for updating `TXT` record and is set to current split text value when unspecified. - `mailbox` (optional): The current email address value. This option is required for updating `RP` record. - `newMailbox` (optional): The new email address value. This option is used for updating `RP` record and is set to the current value when unspecified. - `txtDomain` (optional): The current TXT record's domain name value. This option is required for updating `RP` record. - `newTxtDomain` (optional). The new TXT record's domain name value. This option is used for updating `RP` record and is set to the current value when unspecified. - `priority` (optional): This is the current priority in the SRV record. This parameter is required when updating the `SRV` record. - `newPriority` (optional): This is the new priority in the SRV record. This parameter when missing will use the old value. This parameter is used when updating the `SRV` record. - `weight` (optional): This is the current weight in the SRV record. This parameter is required when updating the `SRV` record. - `newWeight` (optional): This is the new weight in the SRV record. This parameter when missing will use the old value. This parameter is used when updating the `SRV` record. - `port` (optional): This is the port parameter in the SRV record. This parameter is required when updating the `SRV` record. - `newPort` (optional): This is the new value of the port parameter in the SRV record. This parameter when missing will use the old value. This parameter is used to update the port parameter in the `SRV` record. - `target` (optional): The current target value. This parameter is required when updating the `SRV` record. - `newTarget` (optional): The new target value. This parameter when missing will use the old value. This parameter is required when updating the `SRV` record. - `naptrOrder` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record. - `naptrNewOrder` (optional): The new value in the NAPTR record. This parameter when missing will use the old value. This parameter is used when updating the `NAPTR` record. - `naptrPreference` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record. - `naptrNewPreference` (optional): The new value in the NAPTR record. This parameter when missing will use the old value. This parameter is used when updating the `NAPTR` record. - `naptrFlags` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record. - `naptrNewFlags` (optional): The new value in the NAPTR record. This parameter when missing will use the old value. This parameter is used when updating the `NAPTR` record. - `naptrServices` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record. - `naptrNewServices` (optional): The new value in the NAPTR record. This parameter when missing will use the old value. This parameter is used when updating the `NAPTR` record. - `naptrRegexp` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record. - `naptrNewRegexp` (optional): The new value in the NAPTR record. This parameter when missing will use the old value. This parameter is used when updating the `NAPTR` record. - `naptrReplacement` (optional): The current value in the NAPTR record. This parameter is required when updating the `NAPTR` record. - `naptrNewReplacement` (optional): The new value in the NAPTR record. This parameter when missing will use the old value. This parameter is used when updating the `NAPTR` record. - `dname` (optional): The DNAME domain name. This parameter is required when updating the `DNAME` record. - `keyTag` (optional): This parameter is required when updating `DS` record. - `newKeyTag` (optional): This parameter is required when updating `DS` record. - `algorithm` (optional): This parameter is required when updating `DS` record. - `newAlgorithm` (optional): This parameter is required when updating `DS` record. - `digestType` (optional): This parameter is required when updating `DS` record. - `newDigestType` (optional): This parameter is required when updating `DS` record. - `digest` (optional): This parameter is required when updating `DS` record. - `newDigest` (optional): This parameter is required when updating `DS` record. - `sshfpAlgorithm` (optional): This parameter is required when updating `SSHFP` record. - `newSshfpAlgorithm` (optional): This parameter is required when updating `SSHFP` record. - `sshfpFingerprintType` (optional): This parameter is required when updating `SSHFP` record. - `newSshfpFingerprintType` (optional): This parameter is required when updating `SSHFP` record. - `sshfpFingerprint` (optional): This parameter is required when updating `SSHFP` record. - `newSshfpFingerprint` (optional): This parameter is required when updating `SSHFP` record. - `tlsaCertificateUsage` (optional): This parameter is required when updating `TLSA` record. - `newTlsaCertificateUsage` (optional): This parameter is required when updating `TLSA` record. - `tlsaSelector` (optional): This parameter is required when updating `TLSA` record. - `newTlsaSelector` (optional): This parameter is required when updating `TLSA` record. - `tlsaMatchingType` (optional): This parameter is required when updating `TLSA` record. - `newTlsaMatchingType` (optional): This parameter is required when updating `TLSA` record. - `tlsaCertificateAssociationData` (optional): This parameter is required when updating `TLSA` record. - `newTlsaCertificateAssociationData` (optional): This parameter is required when updating `TLSA` record. - `svcPriority` (optional): The priority value for `SVCB` or `HTTPS` record. This parameter is required for updating `SCVB` or `HTTPS` record. - `newSvcPriority` (optional): The new priority value for `SVCB` or `HTTPS` record. This parameter when missing will use the old value. - `svcTargetName` (optional): The target domain name for `SVCB` or `HTTPS` record. This parameter is required for updating `SCVB` or `HTTPS` record. - `newSvcTargetName` (optional): The new target domain name for `SVCB` or `HTTPS` record. This parameter when missing will use the old value. - `svcParams` (optional): The service parameters for `SVCB` or `HTTPS` record which is a pipe separated list of key and value. For example, `alpn|h2,h3|port|53443`. To clear existing values, set it to `false`. This parameter is required for updating `SCVB` or `HTTPS` record. - `newSvcParams` (optional): The new service parameters for `SVCB` or `HTTPS` record which is a pipe separated list of key and value. To clear existing values, set it to `false`. This parameter when missing will use the old value. - `autoIpv4Hint` (optional): Set this option to `true` to enable Automatic Hints for the `ipv4hint` parameter in the `newSvcParams`. This option is valid only for `SVCB` and `HTTPS` records. - `autoIpv6Hint` (optional): Set this option to `true` to enable Automatic Hints for the `ipv6hint` parameter in the `newSvcParams`. This option is valid only for `SVCB` and `HTTPS` records. - `uriPriority` (optional): The priority value for the `URI` record. This parameter is required for updating the `URI` record. - `newUriPriority` (optional): The new priority value for the `URI` record. This parameter when missing will use the old value. - `uriWeight` (optional): The weight value for the `URI` record. This parameter is required for updating the `URI` record. - `newUriWeight` (optional): The new weight value for the `URI` record. This parameter when missing will use the old value. - `uri` (optional): The URI value for the `URI` record. This parameter is required for updating the `URI` record. - `newUri` (optional): The new URI value for the `URI` record. This parameter when missing will use the old value. - `flags` (optional): This is the flags parameter in the `CAA` record. This parameter is required when updating the `CAA` record. - `newFlags` (optional): This is the new value of the flags parameter in the `CAA` record. This parameter is used to update the flags parameter in the `CAA` record. - `tag` (optional): This is the tag parameter in the `CAA` record. This parameter is required when updating the `CAA` record. - `newTag` (optional): This is the new value of the tag parameter in the `CAA` record. This parameter is used to update the tag parameter in the `CAA` record. - `value` (optional): The current value in `CAA` record. This parameter is required when updating the `CAA` record. - `newValue` (optional): The new value in `CAA` record. This parameter is required when updating the `CAA` record. - `aname` (optional): The current `ANAME` domain name. This parameter is required when updating the `ANAME` record. - `newAName` (optional): The new `ANAME` domain name. This parameter is required when updating the `ANAME` record. - `protocol` (optional): This is the current protocol value in the `FWD` record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. This parameter is optional and default value `Udp` will be used when updating the `FWD` record. - `newProtocol` (optional): This is the new protocol value in the `FWD` record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. This parameter is optional and default value `Udp` will be used when updating the `FWD` record. - `forwarder` (optional): The current forwarder address. This parameter is required when updating the `FWD` record. - `newForwarder` (optional): The new forwarder address. This parameter is required when updating the `FWD` record. - `forwarderPriority` (optional): The current forwarder priority value. This optional parameter is to be used with `FWD` record. When unspecified, the default value of `0` will be used. - `dnssecValidation` (optional): Set this boolean value to indicate if DNSSEC validation must be done. This optional parameter is to be used with FWD records. Default value is `false`. - `proxyType` (optional): The type of proxy that must be used for conditional forwarding. This optional parameter is to be used with FWD records. Valid values are [`NoProxy`, `DefaultProxy`, `Http`, `Socks5`]. Default value `DefaultProxy` is used when this parameter is missing. - `proxyAddress` (optional): The proxy server address to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `proxyPort` (optional): The proxy server port to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `proxyUsername` (optional): The proxy server username to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `proxyPassword` (optional): The proxy server password to use when `proxyType` is configured. This optional parameter is to be used with FWD records. - `appName` (optional): This parameter is required for updating the `APP` record. - `classPath` (optional): This parameter is required for updating the `APP` record. - `recordData` (optional): This parameter is used for updating the `APP` record as per the DNS app requirements. - `rdata` (optional): This parameter is used for updating unknown i.e. unsupported record types. The value must be formatted as a hex string or a colon separated hex string. - `newRData` (optional): This parameter is used for updating unknown i.e. unsupported record types. The new value that must be formatted as a hex string or a colon separated hex string. RESPONSE: ``` { "response": { "zone": { "name": "example.com", "type": "Primary", "internal": false, "dnssecStatus": "SignedWithNSEC", "disabled": false }, "updatedRecord": { "disabled": false, "name": "example.com", "type": "SOA", "ttl": 900, "rData": { "primaryNameServer": "server1.home", "responsiblePerson": "hostadmin.example.com", "serial": 75, "refresh": 900, "retry": 300, "expire": 604800, "minimum": 900 }, "dnssecStatus": "Unknown", "lastUsedOn": "0001-01-01T00:00:00" } }, "status": "ok" } ``` ### Delete Record Deletes a record from an authoritative zone. URL:\ `http://localhost:5380/api/zones/records/delete?token=x&domain=example.com&zone=example.com&type=A&value=127.0.0.1` OBSOLETE PATH:\ `/api/zone/deleteRecord`\ `/api/deleteRecord` PERMISSIONS:\ Zones: None\ Zone: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `domain`: The domain name of the zone to delete the record. - `zone` (optional): The name of the authoritative zone into which the `domain` exists. When unspecified, the closest authoritative zone will be used. - `type`: The type of the resource record to delete. - `ipAddress` (optional): This parameter is required when deleting `A` or `AAAA` record. - `updateSvcbHints` (optional): Set this option to `true` to update any SVCB/HTTPS records in the zone that has Automatic Hints option enabled and matches its target name with the current record's domain name. This option is used for `A` and `AAAA` records. - `nameServer` (optional): This parameter is required when deleting `NS` record. - `ptrName` (optional): This parameter is required when deleting `PTR` record. - `preference` (optional): This parameter is required when deleting `MX` record. - `exchange` (optional): This parameter is required when deleting `MX` record. - `text` (optional): This parameter is required when deleting `TXT` record. - `splitText` (optional): This parameter is used when deleting `TXT` record. Default value is set to `false` when unspecified. - `mailbox` (optional): Set an email address for deleting `RP` record. - `txtDomain` (optional): Set a `TXT` record's domain name for deleting `RP` record. - `priority` (optional): This parameter is required when deleting the `SRV` record. - `weight` (optional): This parameter is required when deleting the `SRV` record. - `port` (optional): This parameter is required when deleting the `SRV` record. - `target` (optional): This parameter is required when deleting the `SRV` record. - `naptrOrder` (optional): This parameter is required when deleting the `NAPTR` record. - `naptrPreference` (optional): This parameter is required when deleting the `NAPTR` record. - `naptrFlags` (optional): This parameter is required when deleting the `NAPTR` record. - `naptrServices` (optional): This parameter is required when deleting the `NAPTR` record. - `naptrRegexp` (optional): This parameter is required when deleting the `NAPTR` record. - `naptrReplacement` (optional): This parameter is required when deleting the `NAPTR` record. - `keyTag` (optional): This parameter is required when deleting `DS` record. - `algorithm` (optional): This parameter is required when deleting `DS` record. - `digestType` (optional): This parameter is required when deleting `DS` record. - `digest` (optional): This parameter is required when deleting `DS` record. - `sshfpAlgorithm` (optional): This parameter is required when deleting `SSHFP` record. - `sshfpFingerprintType` (optional): This parameter is required when deleting `SSHFP` record. - `sshfpFingerprint` (optional): This parameter is required when deleting `SSHFP` record. - `tlsaCertificateUsage` (optional): This parameter is required when deleting `TLSA` record. - `tlsaSelector` (optional): This parameter is required when deleting `TLSA` record. - `tlsaMatchingType` (optional): This parameter is required when deleting `TLSA` record. - `tlsaCertificateAssociationData` (optional): This parameter is required when deleting `TLSA` record. - `svcPriority` (optional): The priority value for `SVCB` or `HTTPS` record. This parameter is required for deleting `SCVB` or `HTTPS` record. - `svcTargetName` (optional): The target domain name for `SVCB` or `HTTPS` record. This parameter is required for deleting `SCVB` or `HTTPS` record. - `svcParams` (optional): The service parameters for `SVCB` or `HTTPS` record which is a pipe separated list of key and value. For example, `alpn|h2,h3|port|53443`. To clear existing values, set it to `false`. This parameter is required for deleting `SCVB` or `HTTPS` record. - `uriPriority` (optional): The priority value in the `URI` record. This parameter is required when deleting the `URI` record. - `uriWeight` (optional): The weight value in the `URI` record. This parameter is required when deleting the `URI` record. - `uri` (optional): The URI value in the `URI` record. This parameter is required when deleting the `URI` record. - `flags` (optional): This is the flags parameter in the `CAA` record. This parameter is required when deleting the `CAA` record. - `tag` (optional): This is the tag parameter in the `CAA` record. This parameter is required when deleting the `CAA` record. - `value` (optional): This parameter is required when deleting the `CAA` record. - `aname` (optional): This parameter is required when deleting the `ANAME` record. - `protocol` (optional): This is the protocol parameter in the FWD record. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. This parameter is optional and default value `Udp` will be used when deleting the `FWD` record. - `forwarder` (optional): This parameter is required when deleting the `FWD` record. - `rdata` (optional): This parameter is used for deleting unknown i.e. unsupported record types. The value must be formatted as a hex string or a colon separated hex string. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ## DNS Cache API Calls These API calls allow managing the DNS server cache. ### List Cached Zones List all cached zones. URL:\ `http://localhost:5380/api/cache/list?token=x&domain=google.com` OBSOLETE PATH:\ `/api/listCachedZones` PERMISSIONS:\ Cache: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain` (Optional): The domain name to list records. If not passed, the domain is set to empty string which corresponds to the zone root. - `direction` (Optional): Allows specifying the direction of browsing the zone. Valid values are [`up`, `down`] and the default value is `down` when parameter is missing. This option allows the server to skip empty labels in the domain name when browsing up or down. RESPONSE: ``` { "response": { "domain": "google.com", "zones": [], "records": [ { "name": "google.com", "type": "A", "ttl": "283 (4 mins 43 sec)", "rData": { "value": "216.58.199.174" } } ] }, "status": "ok" } ``` ### Delete Cached Zone Deletes a specific zone from the DNS cache. URL:\ `http://localhost:5380/api/cache/delete?token=x&domain=google.com` OBSOLETE PATH:\ `/api/deleteCachedZone` PERMISSIONS:\ Cache: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain`: The domain name to delete cached records from. RESPONSE: ``` { "status": "ok" } ``` ### Flush DNS Cache This call clears all the DNS cache from the server forcing the DNS server to make recursive queries again to populate the cache. URL:\ `http://localhost:5380/api/cache/flush?token=x` OBSOLETE PATH:\ `/api/flushDnsCache` PERMISSIONS:\ Cache: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "status": "ok" } ``` ## Allowed Zones API Calls These API calls allow managing the Allowed zones. ### List Allowed Zones List all allowed zones. URL:\ `http://localhost:5380/api/allowed/list?token=x&domain=google.com` OBSOLETE PATH:\ `/api/listAllowedZones` PERMISSIONS:\ Allowed: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `domain` (Optional): The domain name to list records. If not passed, the domain is set to empty string which corresponds to the zone root. - `direction` (Optional): Allows specifying the direction of browsing the zone. Valid values are [`up`, `down`] and the default value is `down` when parameter is missing. This option allows the server to skip empty labels in the domain name when browsing up or down. RESPONSE: ``` { "response": { "domain": "google.com", "zones": [], "records": [ { "name": "google.com", "type": "NS", "ttl": "14400 (4 hours)", "rData": { "value": "server1" } }, { "name": "google.com", "type": "SOA", "ttl": "14400 (4 hours)", "rData": { "primaryNameServer": "server1", "responsiblePerson": "hostadmin.server1", "serial": 1, "refresh": 14400, "retry": 3600, "expire": 604800, "minimum": 900 } } ] }, "status": "ok" } ``` ### Allow Zone Adds a domain name into the Allowed Zones. URL:\ `http://localhost:5380/api/allowed/add?token=x&domain=google.com` OBSOLETE PATH:\ `/api/allowZone` PERMISSIONS:\ Allowed: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain`: The domain name for the zone to be added. RESPONSE: ``` { "status": "ok" } ``` ### Delete Allowed Zone Allows deleting a zone from the Allowed Zones. URL:\ `http://localhost:5380/api/allowed/delete?token=x&domain=google.com` OBSOLETE PATH:\ `/api/deleteAllowedZone` PERMISSIONS:\ Allowed: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain`: The domain name for the zone to be deleted. RESPONSE: ``` { "status": "ok" } ``` ### Flush Allowed Zone Flushes the Allowed zone to clear all records. URL:\ `http://localhost:5380/api/allowed/flush?token=x` OBSOLETE PATH:\ `/api/flushAllowedZone` PERMISSIONS:\ Allowed: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "status": "ok" } ``` ### Import Allowed Zones Imports domain names into the Allowed Zones. URL:\ `http://localhost:5380/api/allowed/import?token=x` OBSOLETE PATH:\ `/api/importAllowedZones` PERMISSIONS:\ Allowed: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. REQUEST: This is a `POST` request call where the content type of the request must be `application/x-www-form-urlencoded` and the content must be as shown below: ``` allowedZones=google.com,twitter.com ``` WHERE: - `allowedZones`: A list of comma separated domain names that are to be imported. RESPONSE: ``` { "status": "ok" } ``` ### Export Allowed Zones Allows exporting all the zones from the Allowed Zones as a text file. URL:\ `http://localhost:5380/api/allowed/export?token=x` OBSOLETE PATH:\ `/api/exportAllowedZones` PERMISSIONS:\ Allowed: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: Response is a downloadable text file with `Content-Type: text/plain` and `Content-Disposition: attachment`. ## Blocked Zones API Calls These API calls allow managing the Blocked zones. ### List Blocked Zones List all blocked zones. URL:\ `http://localhost:5380/api/blocked/list?token=x&domain=google.com` OBSOLETE PATH:\ `/api/listBlockedZones` PERMISSIONS:\ Blocked: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `domain` (Optional): The domain name to list records. If not passed, the domain is set to empty string which corresponds to the zone root. - `direction` (Optional): Allows specifying the direction of browsing the zone. Valid values are [`up`, `down`] and the default value is `down` when parameter is missing. This option allows the server to skip empty labels in the domain name when browsing up or down. RESPONSE: ``` { "response": { "domain": "google.com", "zones": [], "records": [ { "name": "google.com", "type": "NS", "ttl": "14400 (4 hours)", "rData": { "value": "server1" } }, { "name": "google.com", "type": "SOA", "ttl": "14400 (4 hours)", "rData": { "primaryNameServer": "server1", "responsiblePerson": "hostadmin.server1", "serial": 1, "refresh": 14400, "retry": 3600, "expire": 604800, "minimum": 900 } } ] }, "status": "ok" } ``` ### Block Zone Adds a domain name into the Blocked Zones. URL:\ `http://localhost:5380/api/blocked/add?token=x&domain=google.com` OBSOLETE PATH:\ `/api/blockZone` PERMISSIONS:\ Blocked: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain`: The domain name for the zone to be added. RESPONSE: ``` { "status": "ok" } ``` ### Delete Blocked Zone Allows deleting a zone from the Blocked Zones. URL:\ `http://localhost:5380/api/blocked/delete?token=x&domain=google.com` OBSOLETE PATH:\ `/api/deleteBlockedZone` PERMISSIONS:\ Blocked: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `domain`: The domain name for the zone to be deleted. RESPONSE: ``` { "status": "ok" } ``` ### Flush Blocked Zone Flushes the Blocked zone to clear all records. URL:\ `http://localhost:5380/api/blocked/flush?token=x` OBSOLETE PATH:\ `/api/flushBlockedZone` PERMISSIONS:\ Blocked: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "status": "ok" } ``` ### Import Blocked Zones Imports domain names into Blocked Zones. URL:\ `http://localhost:5380/api/blocked/import?token=x` OBSOLETE PATH:\ `/api/importBlockedZones` PERMISSIONS:\ Blocked: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. REQUEST: This is a `POST` request call where the content type of the request must be `application/x-www-form-urlencoded` and the content must be as shown below: ``` blockedZones=google.com,twitter.com ``` WHERE: - `blockedZones`: A list of comma separated domain names that are to be imported. RESPONSE: ``` { "status": "ok" } ``` ### Export Blocked Zones Allows exporting all the zones from the Blocked Zones as a text file. URL:\ `http://localhost:5380/api/blocked/export?token=x` OBSOLETE PATH:\ `/api/exportBlockedZones` PERMISSIONS:\ Blocked: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: Response is a downloadable text file with `Content-Type: text/plain` and `Content-Disposition: attachment`. ## DNS Apps API Calls These API calls allows managing DNS Apps. ### List Apps Lists all installed apps on the DNS server. If the DNS server has Internet access and is able to retrieve data from DNS App Store, the API call will also return if a store App has updates available. URL:\ `http://localhost:5380/api/apps/list?token=x` PERMISSIONS:\ Apps/Zones/Logs: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "apps": [ { "name": "Block Page", "version": "1.0", "dnsApps": [ { "classPath": "BlockPageWebServer.App", "description": "Serves a block page from a built-in web server that can be displayed to the end user when a website is blocked by the DNS server.\n\nNote: You need to manually configure the custom IP addresses of this built-in web server in the blocking settings for the block page to be served.", "isAppRecordRequestHandler": false, "isRequestController": false, "isAuthoritativeRequestHandler": false, "isRequestBlockingHandler": false, "isQueryLogger": false, "isPostProcessor": false } ] }, { "name": "What Is My DNS", "version": "2.0", "dnsApps": [ { "classPath": "WhatIsMyDns.App", "description": "Returns the IP address of the user's DNS Server for A, AAAA, and TXT queries.", "isAppRecordRequestHandler": true, "recordDataTemplate": null, "isRequestController": false, "isAuthoritativeRequestHandler": false, "isRequestBlockingHandler": false, "isQueryLogger": false, "isPostProcessor": false } ] } ] }, "status": "ok" } ``` ### List Store Apps Lists all available apps on the DNS App Store. URL:\ `http://localhost:5380/api/apps/listStoreApps?token=x` PERMISSIONS:\ Apps: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "storeApps": [ { "name": "Geo Continent", "version": "1.1", "description": "Returns A or AAAA records, or CNAME record based on the continent the client queries from using MaxMind GeoIP2 Country database. This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. To update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option.", "url": "https://download.technitium.com/dns/apps/GeoContinentApp.zip", "size": "2.01 MB", "installed": false }, { "name": "Geo Country", "version": "1.1", "description": "Returns A or AAAA records, or CNAME record based on the country the client queries from using MaxMind GeoIP2 Country database. This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. To update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option.", "url": "https://download.technitium.com/dns/apps/GeoCountryApp.zip", "size": "2.01 MB", "installed": false }, { "name": "Geo Distance", "version": "1.1", "description": "Returns A or AAAA records, or CNAME record of the server located geographically closest to the client using MaxMind GeoIP2 City database. This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. To update the MaxMind GeoIP2 database for your app, download the GeoIP2-City.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option.", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp.zip", "size": "28.6 MB", "installed": false }, { "name": "Split Horizon", "version": "1.1", "description": "Returns different set of A or AAAA records, or CNAME record for clients querying over public and private networks.", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp.zip", "size": "11.1 KB", "installed": true, "installedVersion": "1.1", "updateAvailable": false }, { "name": "What Is My Dns", "version": "1.1", "description": "Returns the IP address of the user's DNS Server for A, AAAA, and TXT queries.", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp.zip", "size": "8.79 KB", "installed": true, "installedVersion": "1.1", "updateAvailable": false } ] }, "status": "ok" } ``` ### Download And Install App Download an app zip file from given URL and installs it on the DNS Server. URL:\ `http://localhost:5380/api/apps/downloadAndInstall?token=x&name=app-name&url=https://example.com/app.zip` PERMISSIONS:\ Apps: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the app to install. - `url`: The URL of the app zip file. URL must start with `https://`. RESPONSE: ``` { "response": { "installedApp": { "name": "Wild IP", "version": "1.0", "dnsApps": [ { "classPath": "WildIp.App", "description": "Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.", "isAppRecordRequestHandler": true, "recordDataTemplate": null, "isRequestController": false, "isAuthoritativeRequestHandler": false, "isRequestBlockingHandler": false, "isQueryLogger": false, "isPostProcessor": false } ] } }, "status": "ok" } ``` ### Download And Update App Download an app zip file from given URL and updates an existing app installed on the DNS Server. URL:\ `http://localhost:5380/api/apps/downloadAndUpdate?token=x&name=app-name&url=https://example.com/app.zip` PERMISSIONS:\ Apps: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the app to install. - `url`: The URL of the app zip file. URL must start with `https://`. RESPONSE: ``` { "response": { "updatedApp": { "name": "Wild IP", "version": "1.0", "dnsApps": [ { "classPath": "WildIp.App", "description": "Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.", "isAppRecordRequestHandler": true, "recordDataTemplate": null, "isRequestController": false, "isAuthoritativeRequestHandler": false, "isRequestBlockingHandler": false, "isQueryLogger": false, "isPostProcessor": false } ] } }, "status": "ok" } ``` ### Install App Installs a DNS application on the DNS server. URL:\ `http://localhost:5380/api/apps/install?token=x&name=app-name` PERMISSIONS:\ Apps: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the app to install. REQUEST: This is a POST request call where the request must be multi-part form data with the DNS application zip file data in binary format. RESPONSE: ``` { "response": { "installedApp": { "name": "Wild IP", "version": "1.0", "dnsApps": [ { "classPath": "WildIp.App", "description": "Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.", "isAppRecordRequestHandler": true, "recordDataTemplate": null, "isRequestController": false, "isAuthoritativeRequestHandler": false, "isRequestBlockingHandler": false, "isQueryLogger": false, "isPostProcessor": false } ] } }, "status": "ok" } ``` ### Update App Allows to manually update an installed app using a provided app zip file. URL:\ `http://localhost:5380/api/apps/update?token=x&name=app-name` PERMISSIONS:\ Apps: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the app to update. REQUEST: This is a POST request call where the request must be multi-part form data with the DNS application zip file data in binary format. RESPONSE: ``` { "response": { "updatedApp": { "name": "Wild IP", "version": "1.0", "dnsApps": [ { "classPath": "WildIp.App", "description": "Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.", "isAppRecordRequestHandler": true, "recordDataTemplate": null, "isRequestController": false, "isAuthoritativeRequestHandler": false, "isRequestBlockingHandler": false, "isQueryLogger": false, "isPostProcessor": false } ] } }, "status": "ok" } ``` ### Uninstall App Uninstall an app from the DNS server. This does not remove any APP records that were using this DNS application. URL:\ `http://localhost:5380/api/apps/uninstall?token=x&name=app-name` PERMISSIONS:\ Apps: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the app to uninstall. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Get App Config Retrieve the DNS application config from the `dnsApp.config` file in the application folder. URL:\ `http://localhost:5380/api/apps/config/get?token=x&name=app-name` OBSOLETE PATH:\ `/api/apps/getConfig` PERMISSIONS:\ Apps: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `name`: The name of the app to retrieve the config. RESPONSE: ``` { "response": { "config": "config data or `null`" }, "status": "ok" } ``` ### Set App Config Saves the provided DNS application config into the `dnsApp.config` file in the application folder. URL:\ `http://localhost:5380/api/apps/config/set?token=x&name=app-name` OBSOLETE PATH:\ `/api/apps/setConfig` PERMISSIONS:\ Apps: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the app to retrieve the config. REQUEST: This is a POST request call where the content type of the request must be `application/x-www-form-urlencoded` and the content must be as shown below: ``` config=query-string-encoded-config-data ``` RESPONSE: ``` { "response": {}, "status": "ok" } ``` ## DNS Client API Calls These API calls allow interacting with the DNS Client section. ### Resolve Query URL:\ `http://localhost:5380/api/dnsClient/resolve?token=x&server=this-server&domain=example.com&type=A&protocol=UDP` OBSOLETE PATH:\ `/api/resolveQuery` PERMISSIONS:\ DnsClient: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `server`: The name server to query using the DNS client. Use `recursive-resolver` to perform recursive resolution. Use `system-dns` to query the DNS servers configured on the system. - `domain`: The domain name to query. - `type`: The type of the query. - `protocol` (optional): The DNS transport protocol to be used to query. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. The default value of `Udp` is used when the parameter is missing. - `dnssec` (optional): Set to `true` to enable DNSSEC validation. - `eDnsClientSubnet` (optional): The network address to be used with EDNS Client Subnet option in the request. - `import` (optional): This parameter when set to `true` indicates that the response of the DNS query should be imported in the an authoritative zone on this DNS server. Default value is `false` when this parameter is missing. If a zone does not exists, a primary zone for the `domain` name is created and the records from the response are set into the zone. Import can be done only for primary and forwarder type of zones. When `type` is set to AXFR, then the import feature will work as if a zone transfer was requested and the complete zone will be updated as per the zone transfer response. Note that any existing record type for the given `type` will be overwritten when syncing the records. It is recommended to use `recursive-resolver` or the actual name server address for the `server` parameter when importing records. You must have Zones Modify permission to create a zone or Zone Modify permission to import records into an existing zone. RESPONSE: ``` { "response": { "result": { "Metadata": { "NameServer": "server1:53 (127.0.0.1:53)", "Protocol": "Udp", "DatagramSize": "45 bytes", "RoundTripTime": "1.42 ms" }, "Identifier": 60127, "IsResponse": true, "OPCODE": "StandardQuery", "AuthoritativeAnswer": true, "Truncation": false, "RecursionDesired": true, "RecursionAvailable": true, "Z": 0, "AuthenticData": false, "CheckingDisabled": false, "RCODE": "NoError", "QDCOUNT": 1, "ANCOUNT": 1, "NSCOUNT": 0, "ARCOUNT": 0, "Question": [ { "Name": "example.com", "Type": "A", "Class": "IN" } ], "Answer": [ { "Name": "example.com", "Type": "A", "Class": "IN", "TTL": "86400 (1 day)", "RDLENGTH": "4 bytes", "RDATA": { "IPAddress": "127.0.0.1" } } ], "Authority": [], "Additional": [] }, "rawResponses": [] }, "status": "ok" } ``` ## Settings API Calls These API calls allow managing the DNS server settings. ### Get DNS Settings This call returns all the DNS server settings. URL:\ `http://localhost:5380/api/settings/get?token=x` OBSOLETE PATH:\ `/api/getDnsSettings` PERMISSIONS:\ Settings: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "response": { "version": "14.3", "uptimestamp": "2025-05-31T10:28:21.6864142Z", "dnsServerDomain": "server1", "dnsServerLocalEndPoints": [ "0.0.0.0:53", "[::]:53" ], "dnsServerIPv4SourceAddresses": [ "0.0.0.0" ], "dnsServerIPv6SourceAddresses": [ "::" ], "defaultRecordTtl": 3600, "defaultNsRecordTtl": 14400, "defaultSoaRecordTtl": 900, "defaultResponsiblePerson": null, "useSoaSerialDateScheme": false, "minSoaRefresh": 300, "minSoaRetry": 300, "zoneTransferAllowedNetworks": [], "notifyAllowedNetworks": [], "dnsAppsEnableAutomaticUpdate": true, "preferIPv6": false, "enableUdpSocketPool": false, "socketPoolExcludedPorts": [ 53443 ], "udpPayloadSize": 1232, "dnssecValidation": true, "eDnsClientSubnet": false, "eDnsClientSubnetIPv4PrefixLength": 24, "eDnsClientSubnetIPv6PrefixLength": 56, "eDnsClientSubnetIpv4Override": null, "eDnsClientSubnetIpv6Override": null, "qpmPrefixLimitsIPv4": [ { "prefix": 32, "udpLimit": 600, "tcpLimit": 600 }, { "prefix": 24, "udpLimit": 6000, "tcpLimit": 6000 } ], "qpmPrefixLimitsIPv6": [ { "prefix": 128, "udpLimit": 600, "tcpLimit": 600 }, { "prefix": 64, "udpLimit": 1200, "tcpLimit": 1200 }, { "prefix": 56, "udpLimit": 6000, "tcpLimit": 6000 } ], "qpmLimitSampleMinutes": 5, "qpmLimitUdpTruncationPercentage": 50, "qpmLimitBypassList": [], "clientTimeout": 2000, "tcpSendTimeout": 10000, "tcpReceiveTimeout": 10000, "quicIdleTimeout": 60000, "quicMaxInboundStreams": 100, "listenBacklog": 100, "maxConcurrentResolutionsPerCore": 100, "webServiceLocalAddresses": [ "[::]" ], "webServiceHttpPort": 5380, "webServiceEnableTls": true, "webServiceEnableHttp3": false, "webServiceHttpToTlsRedirect": false, "webServiceUseSelfSignedTlsCertificate": true, "webServiceTlsPort": 53443, "webServiceTlsCertificatePath": null, "webServiceTlsCertificatePassword": "************", "webServiceRealIpHeader": "X-Real-IP", "enableDnsOverUdpProxy": false, "enableDnsOverTcpProxy": false, "enableDnsOverHttp": false, "enableDnsOverTls": false, "enableDnsOverHttps": false, "enableDnsOverHttp3": false, "enableDnsOverQuic": false, "dnsOverUdpProxyPort": 538, "dnsOverTcpProxyPort": 538, "dnsOverHttpPort": 80, "dnsOverTlsPort": 853, "dnsOverHttpsPort": 443, "dnsOverQuicPort": 853, "reverseProxyNetworkACL": [], "dnsTlsCertificatePath": null, "dnsTlsCertificatePassword": "************", "dnsOverHttpRealIpHeader": "X-Real-IP", "tsigKeys": [ { "keyName": "home", "sharedSecret": "E9crgbHbzgEI+e+/pBmARRif70ScKf2sc/FjrgnCWyc=", "algorithmName": "hmac-sha256" } ], "recursion": "AllowOnlyForPrivateNetworks", "recursionNetworkACL": [], "randomizeName": false, "qnameMinimization": true, "resolverRetries": 2, "resolverTimeout": 1500, "resolverConcurrency": 2, "resolverMaxStackCount": 16, "saveCache": true, "serveStale": true, "serveStaleTtl": 259200, "serveStaleAnswerTtl": 30, "serveStaleResetTtl": 30, "serveStaleMaxWaitTime": 1800, "cacheMaximumEntries": 10000, "cacheMinimumRecordTtl": 10, "cacheMaximumRecordTtl": 604800, "cacheNegativeRecordTtl": 300, "cacheFailureRecordTtl": 10, "cachePrefetchEligibility": 2, "cachePrefetchTrigger": 9, "cachePrefetchSampleIntervalInMinutes": 5, "cachePrefetchSampleEligibilityHitsPerHour": 30, "enableBlocking": true, "allowTxtBlockingReport": true, "blockingBypassList": [], "blockingType": "NxDomain", "blockingAnswerTtl": 30, "customBlockingAddresses": [ "127.0.0.1" ], "blockListUrls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", "https://big.oisd.nl/" ], "blockListUpdateIntervalHours": 24, "blockListNextUpdatedOn": "2024-02-01T20:15:08.658124Z", "proxy": null, "forwarders": null, "forwarderProtocol": "Udp", "concurrentForwarding": true, "forwarderRetries": 3, "forwarderTimeout": 2000, "forwarderConcurrency": 2, "enableLogging": true, "loggingType": "File", "ignoreResolverLogs": false, "logQueries": false, "useLocalTime": false, "logFolder": "logs", "maxLogFileDays": 30, "enableInMemoryStats": false, "maxStatFileDays": 365 }, "status": "ok" } ``` ### Set DNS Settings This call allows to change the DNS server settings. Note! Any parameter passed with this API call will overwrite existing value for that parameter. If you wish to append new values instead then you should first call the Get DNS Settings API to get the existing value, append your new value to it, and then pass the updated value with this API call. URL:\ `http://localhost:5380/api/settings/set?token=x&dnsServerDomain=server1&dnsServerLocalEndPoints=0.0.0.0:53,[::]:53&webServiceLocalAddresses=0.0.0.0,[::]&webServiceHttpPort=5380&webServiceEnableTls=false&webServiceTlsPort=53443&webServiceTlsCertificatePath=&webServiceTlsCertificatePassword=&enableDnsOverHttp=false&enableDnsOverTls=false&enableDnsOverHttps=false&dnsTlsCertificatePath=&dnsTlsCertificatePassword=&preferIPv6=false&logQueries=true&allowRecursion=true&allowRecursionOnlyForPrivateNetworks=true&randomizeName=true&cachePrefetchEligibility=2&cachePrefetchTrigger=9&cachePrefetchSampleIntervalInMinutes=5&cachePrefetchSampleEligibilityHitsPerHour=30&proxyType=socks5&proxyAddress=192.168.10.2&proxyPort=9050&proxyUsername=username&proxyPassword=password&proxyBypass=127.0.0.0/8,169.254.0.0/16,fe80::/10,::1,localhost&forwarders=192.168.10.2&forwarderProtocol=Udp&useNxDomainForBlocking=false&blockListUrls=https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts,https://mirror1.malwaredomains.com/files/justdomains,https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt,https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt` OBSOLETE PATH:\ `/api/setDnsSettings` PERMISSIONS:\ Settings: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `dnsServerDomain` (optional): The primary domain name used by this DNS Server to identify itself. - `dnsServerLocalEndPoints` (optional): Local end points are the network interface IP addresses and ports you want the DNS Server to listen for requests. - `dnsServerIPv4SourceAddresses` (optional): A comma separated list of IPv4 source addresses that the DNS server must use for making all outbound DNS requests when the server is connected to two or more networks. Network addresses are also accepted. By default, the IPv4 address of the network with a default route will be used as the source address. - `dnsServerIPv6SourceAddresses` (optional): A comma separated list of IPv6 source addresses that the DNS server must use for making all outbound DNS requests when the server is connected to two or more networks. Network addresses are also accepted. By default, the IPv6 address of the network with a default route will be used as the source address. Note that this option will be used only when `Prefer IPv6` option is enabled. - `defaultRecordTtl` (optional, cluster parameter): The default TTL value to use if not specified when adding or updating records in a Zone. - `defaultNsRecordTtl` (optional, cluster parameter): The default TTL value to use if not specified when adding or updating NS records in a Primary Zone. - `defaultSoaRecordTtl` (optional, cluster parameter): The default TTL value to use if not specified when adding or updating SOA records in a Primary Zone. - `defaultResponsiblePerson` (optional, cluster parameter): The default SOA Responsible Person email address to use when adding a Primary Zone. - `useSoaSerialDateScheme` (optional, cluster parameter): The default SOA Serial option to use if not specified when adding a Primary Zone. - `minSoaRefresh` (optional, cluster parameter): The minimum Refresh interval to be used by Secondary, Stub, Secondary Forwarder, and Secondary Catalog zones. This value will be used if a zone's SOA Refresh value is less than the minimum value. Initial value is `300`. - `minSoaRetry` (optional, cluster parameter): The minimum Retry interval to be used by Secondary, Stub, Secondary Forwarder, and Secondary Catalog zones zones. This value will be used if a zone's SOA Retry value is less than the minimum value. Initial value is `300`. - `zoneTransferAllowedNetworks` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to perform zone transfer for all zones without any TSIG authentication. - `notifyAllowedNetworks` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to Notify all secondary zones. - `dnsAppsEnableAutomaticUpdate` (optional, cluster parameter): Set to `true` to allow DNS server to automatically update the DNS Apps from the DNS App Store. The DNS Server will check for updates every 24 hrs when this option is enabled. - `preferIPv6` (optional): DNS Server will use IPv6 for querying whenever possible with this option enabled. Initial value is `false`. - `enableUdpSocketPool` (optional): Set this to `true` to enable UDP socket pool. The DNS Server will use UDP socket pool for all outbound DNS-over-UDP requests when enabled. - `socketPoolExcludedPorts` (optional): A comma separated list of port numbers that must be excluded from being used by the UDP socket pool. - `udpPayloadSize` (optional, cluster parameter): The maximum EDNS UDP payload size that can be used to avoid IP fragmentation. Valid range is 512-4096 bytes. Initial value is `1232`. - `dnssecValidation` (optional, cluster parameter): Set this to `true` to enable DNSSEC validation. DNS Server will validate all responses from name servers or forwarders when this option is enabled. - `eDnsClientSubnet` (optional, cluster parameter): Set this to `true` to enable EDNS Client Subnet. DNS Server will use the public IP address of the request with a prefix length, or the existing Client Subnet option from the request while resolving requests. - `eDnsClientSubnetIPv4PrefixLength` (optional, cluster parameter): The EDNS Client Subnet IPv4 prefix length to define the client subnet. Initial value is `24`. - `eDnsClientSubnetIPv6PrefixLength` (optional, cluster parameter): The EDNS Client Subnet IPv6 prefix length to define the client subnet. Initial value is `56`. - `eDnsClientSubnetIpv4Override` (optional, cluster parameter): The IPv4 network address that must be used as ECS for all outbound requests overriding client's actual subnet. - `eDnsClientSubnetIpv6Override` (optional, cluster parameter): The IPv6 network address that must be used as ECS for all outbound requests overriding client's actual subnet. - `qpmPrefixLimitsIPv4` (optional, cluster parameter): A pipe `|` separated multi row list of prefix, udpLimit and tcpLimit. Set this parameter to `false` to remove all entries. The maximum queries an IPv4 client subnet can make to DNS-over-UDP and DNS-over-TCP protocol services per minute on average based on the sample size. Set limit value to 0 to allow unlimited queries. - `qpmPrefixLimitsIPv6` (optional, cluster parameter): A pipe `|` separated multi row list of prefix, udpLimit and tcpLimit. Set this parameter to `false` to remove all entries. The maximum queries an IPv6 client subnet can make to DNS-over-UDP and DNS-over-TCP protocol services per minute on average based on the sample size. Set limit value to 0 to allow unlimited queries. - `qpmLimitSampleMinutes` (optional, cluster parameter): Sets the client query stats sample size in minutes for QPM limit feature. Initial value is `5`. - `qpmLimitUdpTruncationPercentage` (optional, cluster parameter): The percentage of requests that are responded with a truncation (TC) response when QPM limit exceeds for DNS-over-UDP protocol service while the rest of the requests are dropped. A TC response will cause a real client to retry to DNS-over-TCP protocol service. Valid range is `0`-`100`. Initial value is `50`. - `qpmLimitBypassList` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to bypass the QPM limit. - `clientTimeout` (optional, cluster parameter): The amount of time the DNS server must wait in milliseconds before responding with a ServerFailure response to a client request when no answer is available. Valid range is `1000`-`10000`. Initial value is `4000`. - `tcpSendTimeout` (optional, cluster parameter): The maximum amount of time in milliseconds a TCP socket will wait for the response to be sent. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, TcpProxy, or HTTPS transports. Valid range is `1000`-`90000`. Initial value is `10000`. - `tcpReceiveTimeout` (optional, cluster parameter): The maximum amount of time in milliseconds a TCP socket will wait for receiving data. This option will apply for DNS requests being received by the DNS Server over TCP, TLS, TcpProxy, or HTTPS transports. Valid range is `1000`-`90000`. Initial value is `10000`. - `quicIdleTimeout` (optional, cluster parameter): The time interval in milliseconds after which an idle QUIC connection will be closed. This option applies only to QUIC transport protocol. Valid range is `1000`-`90000`. Initial value is `60000`. - `quicMaxInboundStreams` (optional, cluster parameter): The max number of inbound bidirectional streams that can be accepted per QUIC connection. This option applies only to QUIC transport protocol. Valid range is `1`-`1000`. Initial value is `100`. - `listenBacklog` (optional, cluster parameter): The maximum number of pending inbound connections. This option applies to TCP, TLS, TcpProxy, and QUIC transport protocols. Initial value is `100`. - `maxConcurrentResolutionsPerCore` (optional, cluster parameter): The maximum number of concurrent async outbound resolutions that should be done per CPU core. Initial value is `100`. - `webServiceLocalAddresses` (optional): Local addresses are the network interface IP addresses you want the web service to listen for requests. - `webServiceHttpPort` (optional): Specify the TCP port number for the web console and this API web service. Initial value is `5380`. - `webServiceEnableTls` (optional): Set this to `true` to start the HTTPS service to access web service. - `webServiceEnableHttp3` (optional): Set this to `true` to enable HTTP/3 protocol for the web service. - `webServiceHttpToTlsRedirect` (optional): Set this option to `true` to enable HTTP to HTTPS Redirection. - `webServiceTlsPort` (optional): Specified the TCP port number for the web console for HTTPS access. - `webServiceUseSelfSignedTlsCertificate` (optional): Set `true` for the web service to use an automatically generated self signed certificate when TLS certificate path is not specified. - `webServiceTlsCertificatePath` (optional): Specify a PKCS #12 certificate (.pfx) file path on the server. The certificate must contain private key. This certificate is used by the web console for HTTPS access. - `webServiceTlsCertificatePassword` (optional): Enter the certificate (.pfx) password, if any. - `webServiceRealIpHeader` (optional): The HTTP header that must be used to read client's actual IP address when the request comes from a reverse proxy with a private IP address. - `enableDnsOverUdpProxy` (optional): Enable this option to accept DNS-over-UDP-PROXY requests. It implements the [PROXY Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) for both version 1 & 2 over UDP datagram. Configure the `reverseProxyNetworkACL` option to allow only requests coming from your reverse proxy server. - `enableDnsOverTcpProxy` (optional): Enable this option to accept DNS-over-TCP-PROXY requests. It implements the [PROXY Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) for both version 1 & 2 over TCP connection. Configure the `reverseProxyNetworkACL` option to allow only requests coming from your reverse proxy server. - `enableDnsOverHttp` (optional): Enable this option to accept DNS-over-HTTP requests. It must be used with a TLS terminating reverse proxy like nginx. Configure the `reverseProxyNetworkACL` option to allow only requests coming from your reverse proxy server. Enabling this option also allows automatic TLS certificate renewal with HTTP challenge (webroot) for DNS-over-HTTPS service. - `enableDnsOverTls` (optional): Enable this option to accept DNS-over-TLS requests. - `enableDnsOverHttps` (optional): Enable this option to accept DNS-over-HTTPS requests. - `enableDnsOverQuic` (optional): Enable this option to accept DNS-over-QUIC requests. - `dnsOverUdpProxyPort` (optional): The UDP port number for DNS-over-UDP-PROXY protocol. Initial value is `538`. - `dnsOverTcpProxyPort` (optional): The TCP port number for DNS-over-TCP-PROXY protocol. Initial value is `538`. - `dnsOverHttpPort` (optional): The TCP port number for DNS-over-HTTP protocol. Initial value is `80`. - `dnsOverTlsPort` (optional): The TCP port number for DNS-over-TLS protocol. Initial value is `853`. - `dnsOverHttpsPort` (optional): The TCP port number for DNS-over-HTTPS protocol. Initial value is `443`. - `dnsOverQuicPort` (optional): The UDP port number for DNS-over-QUIC protocol. Initial value is `853`. - `reverseProxyNetworkACL` (optional): Configure the ACL to allow only requests coming from your reverse proxy server for DNS-over-UDP-PROXY, DNS-over-TCP-PROXY, and DNS-over-HTTP protocols. Enter IP addresses or network addresses one below another to allow access. Add ! character at the start to deny access, e.g. !192.168.10.0/24 will deny entire subnet. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all. - `dnsTlsCertificatePath` (optional): Specify a PKCS #12 certificate (.pfx) file path on the server. The certificate must contain private key. This certificate is used by the DNS-over-TLS and DNS-over-HTTPS optional protocols. - `dnsTlsCertificatePassword` (optional): Enter the certificate (.pfx) password, if any. - `dnsOverHttpRealIpHeader` (optional): The HTTP header that must be used to read client's actual IP address when the request comes from a reverse proxy with a private IP address. - `tsigKeys` (optional, cluster parameter): A pipe `|` separated multi row list of TSIG key name, shared secret, and algorithm. Set this parameter to `false` to remove all existing keys. Supported algorithms are [`hmac-md5.sig-alg.reg.int`, `hmac-sha1`, `hmac-sha256`, `hmac-sha256-128`, `hmac-sha384`, `hmac-sha384-192`, `hmac-sha512`, `hmac-sha512-256`]. - `recursion` (optional, cluster parameter): Sets the recursion policy for the DNS server. Valid values are [`Deny`, `Allow`, `AllowOnlyForPrivateNetworks`, `UseSpecifiedNetworkACL`]. - `recursionNetworkACL` (optional, cluster parameter): A comma separated Access Control List (ACL) of Network Access Control (NAC) entry. NAC is an IP address or network address to allow. Add `!` at the start of the NAC to deny access. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all except loopback. Set this parameter to `false` to remove existing values. These values are only used when `recursion` is set to `UseSpecifiedNetworkACL`. - `randomizeName` (optional, cluster parameter): Enables QNAME randomization [draft-vixie-dnsext-dns0x20-00](https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00) when using UDP as the transport protocol. Initial value is `true`. - `qnameMinimization` (optional, cluster parameter): Enables QNAME minimization [draft-ietf-dnsop-rfc7816bis-04](https://tools.ietf.org/html/draft-ietf-dnsop-rfc7816bis-04) when doing recursive resolution. Initial value is `true`. - `resolverRetries` (optional, cluster parameter): The number of retries that the recursive resolver must do. - `resolverTimeout` (optional, cluster parameter): The timeout value in milliseconds for the recursive resolver. - `resolverConcurrency` (optional, cluster parameter): The number of concurrent requests that should be sent by the recursive resolver to the name servers. - `resolverMaxStackCount` (optional, cluster parameter): The max stack count that the recursive resolver must use. - `saveCache` (optional): Enable this option to save DNS cache on disk when the DNS server stops. The saved cache will be loaded next time the DNS server starts. - `serveStale` (optional): Enable the serve stale feature to improve resiliency by using expired or stale records in cache when the DNS server is unable to reach the upstream or authoritative name servers. Initial value is `true`. - `serveStaleTtl` (optional): The TTL value in seconds which should be used for cached records that are expired. When the serve stale TTL too expires for a stale record, it gets removed from the cache. Recommended value is between 1-3 days and maximum supported value is 7 days. Initial value is `259200`. - `serveStaleAnswerTtl` (optional): The TTL value in seconds which should be used for the records in a stale response. This is the TTL value that the client will be using to cache the stale records. The valid range is 0-300 seconds and recommended value is 30 seconds. - `serveStaleResetTtl` (optional): The TTL value in seconds which should be used to reset the stale record's TTL value in the cache when the resolver fails to refresh the data. The TTL reset causes the stale records to become valid again so that they can be used to serve requests normally. This reset effectively prevents the resolver from attempting to frequently update the stale records. The valid range is 10-900 seconds and recommended value is 30 seconds. - `serveStaleMaxWaitTime` (optional): The time in milliseconds that the DNS server must wait for the resolver before serving stale records from the cache. Lower value will ensure faster response at the expense of not getting updated data from the upstream. Setting value to 0 will instantly return stale answer without waiting for the resolver to fetch updates from the upstream. The valid range is 0-1800 milliseconds and default value is 1800 milliseconds. - `cacheMinimumRecordTtl` (optional): The minimum TTL value that a record can have in cache. Set a value to make sure that the records with TTL value than it stays in cache for a minimum duration. Initial value is `10`. - `cacheMaximumRecordTtl` (optional): The maximum TTL value that a record can have in cache. Set a lower value to allow the records to expire early. Initial value is `86400`. - `cacheNegativeRecordTtl` (optional): The negative TTL value to use when there is no SOA MINIMUM value available. Initial value is `300`. - `cacheFailureRecordTtl` (optional): The failure TTL value to used for caching failure responses. This allows storing failure record in cache and prevent frequent recursive resolution to name servers that are responding with `ServerFailure`. Initial value is `60`. - `cachePrefetchEligibility` (optional): The minimum initial TTL value of a record needed to be eligible for prefetching. - `cachePrefetchTrigger` (optional): A record with TTL value less than trigger value will initiate prefetch operation immediately for itself. Set `0` to disable prefetching & auto prefetching. - `cachePrefetchSampleIntervalInMinutes` (optional): The interval to sample eligible domain names from last hour stats for auto prefetch. - `cachePrefetchSampleEligibilityHitsPerHour` (optional): Minimum required hits per hour for a domain name to be eligible for auto prefetch. - `enableBlocking` (optional, cluster parameter): Sets the DNS server to block domain names using Blocked Zone and Block List Zone. - `allowTxtBlockingReport` (optional, cluster parameter): Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. - `blockingBypassList` (optional, cluster parameter): A comma separated list of IP addresses or network addresses that are allowed to bypass blocking. - `blockingType` (optional, cluster parameter): Sets how the DNS server should respond to a blocked domain request. Valid values are [`AnyAddress`, `NxDomain`, `CustomAddress`] where `AnyAddress` is default which response with `0.0.0.0` and `::` IP addresses for blocked domains. Using `NxDomain` will respond with `NX Domain` response. `CustomAddress` will return the specified custom blocking addresses. - `blockingAnswerTtl` (optional, cluster parameter): The TTL value in seconds that must be used for the records in a blocking response. This is the TTL value that the client will use to cache the blocking response. - `customBlockingAddresses` (optional, cluster parameter): A comma separated list of IP addresses. Set the custom blocking addresses to be used for blocked domain response. These addresses are returned only when `blockingType` is set to `CustomAddress`. - `blockListUrls` (optional, cluster parameter): A comma separated list of block list URLs that this server must automatically download and use with the block lists zone. DNS Server will use the data returned by the block list URLs to update the block list zone automatically every 24 hours. The expected file format is standard hosts file format or plain text file containing list of domains to block. Set this parameter to `false` to remove existing values. - `blockListUpdateIntervalHours` (optional, cluster parameter): The interval in hours to automatically download and update the block lists. Initial value is `24`. - `proxyType` (optional, cluster parameter): The type of proxy protocol to be used. Valid values are [`None`, `Http`, `Socks5`]. - `proxyAddress` (optional, cluster parameter): The proxy server hostname or IP address. - `proxyPort` (optional, cluster parameter): The proxy server port. - `proxyUsername` (optional, cluster parameter): The proxy server username. - `proxyPassword` (optional, cluster parameter): The proxy server password. - `proxyBypass` (optional, cluster parameter): A comma separated bypass list consisting of IP addresses, network addresses in CIDR format, or host/domain names to never use proxy for. - `forwarders` (optional, cluster parameter): A comma separated list of forwarders to be used by this DNS server. Set this parameter to `false` string to remove existing forwarders so that the DNS server does recursive resolution by itself. - `forwarderProtocol` (optional, cluster parameter): The forwarder DNS transport protocol to be used. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. - `concurrentForwarding` (optional, cluster parameter): Set this option to `true` to allow querying two or more forwarders concurrently instead of sequentially querying them in their given order. The DNS server will automatically select forwarders (based on their average latency) to query and use the fastest response it receives from any of them. If none of the selected forwarders respond in time, the DNS server will similarly select forwarders from the remaining ones and queries them till all are tried before giving up. - `forwarderRetries` (optional, cluster parameter): The number of retries that the forwarder DNS client must do. - `forwarderTimeout` (optional, cluster parameter): The timeout value in milliseconds for the forwarder DNS client. - `forwarderConcurrency` (optional, cluster parameter): The number of concurrent requests that the forwarder DNS client should do. - `loggingType` (optional): Specifies how the error logs and audit logs are written. The valid values are [`None`, `File`, `Console`, `FileAndConsole`]. Initial value is `File`. - `enableLogging` (optional)(obsolete, use `loggingType` instead): Enable this option to log error and audit logs into the log file. Initial value is `true`. - `ignoreResolverLogs` (optional): Enable this option to stop logging domain name resolution errors into the log file. - `logQueries` (optional): Enable this option to log every query received by this DNS Server and the corresponding response answers into the log file. Initial value is `false`. - `useLocalTime` (optional): Enable this option to use local time instead of UTC for logging. Initial value is `false`. - `logFolder` (optional): The folder path on the server where the log files should be saved. The path can be relative to the DNS server config folder. Initial value is `logs`. - `maxLogFileDays` (optional): Max number of days to keep the log files. Log files older than the specified number of days will be deleted automatically. Recommended value is `365`. Set `0` to disable auto delete. - `enableInMemoryStats` (optional): Set this option to `true` to enable in-memory stats. When enabled, only Last Hour data will be available on Dashboard and no stats data will be stored on disk. - `maxStatFileDays` (optional): Max number of days to keep the dashboard stats. Stat files older than the specified number of days will be deleted automatically. Recommended value is `365`. Set `0` to disable auto delete. NOTE! The parameters marked as a "cluster parameter" are synced automatically across all the Cluster nodes when Clustering is initialized. REQUEST: Instead of query string or form data parameters described above, the request optionally can also POST settings as JSON data in the same format as returned by `getDnsSettings` API call. RESPONSE: This call returns the newly updated settings in the same format as that of the `getDnsSettings` call. ### Get TSIG Key Names Returns a list of TSIG key names that are configured in the DNS server Settings. URL:\ `http://localhost:5380/api/settings/getTsigKeyNames?token=x` PERMISSIONS:\ Settings: View OR Zones: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "response": { "tsigKeyNames": [ "key1", "key2" ] }, "status": "ok" } ``` ### Force Update Block Lists This call allows to reset the next update schedule and force download and update of the block lists. URL:\ `http://localhost:5380/api/settings/forceUpdateBlockLists?token=x` OBSOLETE PATH:\ `/api/forceUpdateBlockLists` PERMISSIONS:\ Settings: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "status": "ok" } ``` NOTE! This API call is synced automatically across all Cluster nodes when Clustering is initialized. ### Temporarily Disable Block Lists This call temporarily disables the block lists and block list zones. URL:\ `http://localhost:5380/api/settings/temporaryDisableBlocking?token=x&minutes=5` OBSOLETE PATH:\ `/api/temporaryDisableBlocking` PERMISSIONS:\ Settings: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `minutes`: The time in minutes to disable the blocklist for. RESPONSE: ``` { "status": "ok", "response": { "temporaryDisableBlockingTill": "2021-10-10T01:14:27.1106773Z" } } ``` NOTE! This API call is synced automatically across all Cluster nodes when Clustering is initialized. ### Backup Settings This call returns a zip file containing copies of all the items that were requested to be backed up. URL:\ `http://localhost:5380/api/settings/backup?token=x&blockLists=true&logs=true&scopes=true&stats=true&zones=true&allowedZones=true&blockedZones=true&dnsSettings=true&logSettings=true&authConfig=true` OBSOLETE PATH:\ `/api/backupSettings` PERMISSIONS:\ Settings: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `authConfig` (optional): Set to `true` to backup the authentication config file. Default value is `false`. - `clusterConfig` (optional): Set to `true` to backup the Cluster config file. Default value is `false`. - `webServiceSettings` (optional): Set to `true` to backup the web service config file. Default value is `false`. - `dnsSettings` (optional): Set to `true` to backup DNS settings and certificate files. The Web Service or Optional Protocols TLS certificate (.pfx) files will be included in the backup only if they exist within the DNS server's config folder. Default value is `false`. - `logSettings` (optional): Set to `true` to backup log settings file. Default value is `false`. - `zones` (optional): Set to `true` to backup DNS zone files. Default value is `false`. - `allowedZones` (optional): Set to `true` to backup allowed zones file. Default value is `false`. - `blockedZones` (optional): Set to `true` to backup blocked zones file. Default value is `false`. - `blockLists` (optional): Set to `true` to backup block lists cache files. Default value is `false`. - `apps` (optional): Set to `true` to backup the installed DNS apps. Default value is `false`. - `scopes` (optional): Set to `true` to backup DHCP scope files. Default value is `false`. - `stats` (optional): Set to `true` to backup dashboard stats files. Default value is `false`. - `logs` (optional): Set to `true` to backup log files. Default value is `false`. RESPONSE: A zip file with content type `application/zip` and content disposition set to `attachment`. ### Restore Settings This call restores selected items from a given backup zip file. URL:\ `http://localhost:5380/api/settings/restore?token=x&blockLists=true&logs=true&scopes=true&stats=true&zones=true&allowedZones=true&blockedZones=true&dnsSettings=true&logSettings=true&deleteExistingFiles=true&authConfig=true` OBSOLETE PATH:\ `/api/restoreSettings` PERMISSIONS:\ Settings: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `authConfig` (optional): Set to `true` to restore the authentication config file. Default value is `false`. - `clusterConfig` (optional): Set to `true` to restore the Cluster config file. Default value is `false`. - `webServiceSettings` (optional): Set to `true` to restore the web service config file. Default value is `false`. - `dnsSettings` (optional): Set to `true` to restore DNS settings and certificate files. Default value is `false`. - `logSettings` (optional): Set to `true` to restore log settings file. Default value is `false`. - `zones` (optional): Set to `true` to restore DNS zone files. Default value is `false`. - `allowedZones` (optional): Set to `true` to restore allowed zones file. Default value is `false`. - `blockedZones` (optional): Set to `true` to restore blocked zones file. Default value is `false`. - `blockLists` (optional): Set to `true` to restore block lists cache files. Default value is `false`. - `apps` (optional): Set to `true` to restore the DNS apps. Default value is `false`. - `scopes` (optional): Set to `true` to restore DHCP scope files. Default value is `false`. - `stats` (optional): Set to `true` to restore dashboard stats files. Default value is `false`. - `logs` (optional): Set to `true` to restore log files. Default value is `false`. - `deleteExistingFiles` (optional). Set to `true` to delete existing files for selected items. Default value is `false`. REQUEST: This is a `POST` request call where the request must be multi-part form data with the backup zip file data in binary format. RESPONSE: This call returns the newly updated settings in the same format as that of the `getDnsSettings` call. ## DHCP API Calls Allows managing the built-in DHCP server. ### List DHCP Leases Lists all the DHCP leases. URL:\ `http://localhost:5380/api/dhcp/leases/list?token=x` OBSOLETE PATH:\ `/api/listDhcpLeases` PERMISSIONS:\ DhcpServer: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "response": { "leases": [ { "scope": "Default", "type": "Reserved", "hardwareAddress": "00-00-00-00-00-00", "clientIdentifier": "1-000000000000", "address": "192.168.1.5", "hostName": "server1.local", "leaseObtained": "08/25/2020 17:52:51", "leaseExpires": "09/26/2020 14:27:12" }, { "scope": "Default", "type": "Dynamic", "hardwareAddress": "00-00-00-00-00-00", "clientIdentifier": "1-000000000000", "address": "192.168.1.13", "hostName": null, "leaseObtained": "06/15/2020 16:41:46", "leaseExpires": "09/25/2020 12:39:54" }, { "scope": "Default", "type": "Dynamic", "hardwareAddress": "00-00-00-00-00-00", "clientIdentifier": "1-000000000000", "address": "192.168.1.15", "hostName": "desktop-ea2miaf.local", "leaseObtained": "06/18/2020 12:19:03", "leaseExpires": "09/25/2020 12:17:11" }, ] }, "status": "ok" } ``` ### Remove DHCP Lease Removes a dynamic or reserved lease allocation. This API must be used carefully to make sure that there is no IP address conflict caused by removing a lease. URL:\ `http://localhost:5380/api/dhcp/leases/remove?token=x&name=Default&hardwareAddress=00:00:00:00:00:00` OBSOLETE PATH:\ `/api/removeDhcpLease` PERMISSIONS:\ DhcpServer: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. - `clientIdentifier` (optional): The client identifier for the lease. Either `hardwareAddress` or `clientIdentifier` must be specified. - `hardwareAddress` (optional): The MAC address of the device bearing the dynamic/reserved lease. Either `hardwareAddress` or `clientIdentifier` must be specified. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Convert To Reserved Lease Converts a dynamic lease to reserved lease. URL:\ `http://localhost:5380/api/dhcp/leases/convertToReserved?token=x&name=Default&hardwareAddress=00:00:00:00:00:00` OBSOLETE PATH:\ `/api/convertToReservedLease` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. - `clientIdentifier` (optional): The client identifier for the lease. Either `hardwareAddress` or `clientIdentifier` must be specified. - `hardwareAddress` (optional): The MAC address of the device bearing the dynamic lease. Either `hardwareAddress` or `clientIdentifier` must be specified. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Convert To Dynamic Lease Converts a reserved lease to dynamic lease. URL:\ `http://localhost:5380/api/dhcp/leases/convertToDynamic?token=x&name=Default&hardwareAddress=00:00:00:00:00:00` OBSOLETE PATH:\ `/api/convertToDynamicLease` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. - `clientIdentifier` (optional): The client identifier for the lease. Either `hardwareAddress` or `clientIdentifier` must be specified. - `hardwareAddress` (optional): The MAC address of the device bearing the reserved lease. Either `hardwareAddress` or `clientIdentifier` must be specified. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### List DHCP Scopes Lists all the DHCP scopes available on the server. URL:\ `http://localhost:5380/api/dhcp/scopes/list?token=x` OBSOLETE PATH:\ `/api/listDhcpScopes` PERMISSIONS:\ DhcpServer: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. RESPONSE: ``` { "response": { "scopes": [ { "name": "Default", "enabled": false, "startingAddress": "192.168.1.1", "endingAddress": "192.168.1.254", "subnetMask": "255.255.255.0", "networkAddress": "192.168.1.0", "broadcastAddress": "192.168.1.255" } ] }, "status": "ok" } ``` ### Get DHCP Scope Gets the complete details of the scope configuration. URL:\ `http://localhost:5380/api/dhcp/scopes/get?token=x&name=Default` OBSOLETE PATH:\ `/api/getDhcpScope` PERMISSIONS:\ DhcpServer: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. RESPONSE: ``` { "response": { "name": "Default", "startingAddress": "192.168.1.1", "endingAddress": "192.168.1.254", "subnetMask": "255.255.255.0", "leaseTimeDays": 7, "leaseTimeHours": 0, "leaseTimeMinutes": 0, "offerDelayTime": 0, "pingCheckEnabled": false, "pingCheckTimeout": 1000, "pingCheckRetries": 2, "domainName": "local", "domainSearchList": [ "home.arpa", "lan" ], "dnsUpdates": true, "dnsOverwriteForDynamicLease": false, "dnsTtl": 900, "serverAddress": "192.168.1.1", "serverHostName": "tftp-server-1", "bootFileName": "boot.bin", "routerAddress": "192.168.1.1", "useThisDnsServer": false, "dnsServers": [ "192.168.1.5" ], "winsServers": [ "192.168.1.5" ], "ntpServers": [ "192.168.1.5" ], "staticRoutes": [ { "destination": "172.16.0.0", "subnetMask": "255.255.255.0", "router": "192.168.1.2" } ], "vendorInfo": [ { "identifier": "substring(vendor-class-identifier,0,9)==\"PXEClient\"", "information": "06:01:03:0A:04:00:50:58:45:09:14:00:00:11:52:61:73:70:62:65:72:72:79:20:50:69:20:42:6F:6F:74:FF" } ], "capwapAcIpAddresses": [ "192.168.1.2" ], "tftpServerAddresses": [ "192.168.1.5", "192.168.1.6" ], "genericOptions": [ { "code": 150, "value": "C0:A8:01:01" } ], "exclusions": [ { "startingAddress": "192.168.1.1", "endingAddress": "192.168.1.10" } ], "reservedLeases": [ { "hostName": null, "hardwareAddress": "00-00-00-00-00-00", "address": "192.168.1.10", "comments": "comments" } ], "allowOnlyReservedLeases": false, "blockLocallyAdministeredMacAddresses": true, "ignoreClientIdentifierOption": true }, "status": "ok" } ``` ### Set DHCP Scope Sets the DHCP scope configuration. URL:\ `http://localhost:5380/api/dhcp/scopes/set?token=x&name=Default&startingAddress=192.168.1.1&endingAddress=192.168.1.254&subnetMask=255.255.255.0&leaseTimeDays=7&leaseTimeHours=0&leaseTimeMinutes=0&offerDelayTime=0&domainName=local&dnsTtl=900&serverAddress=&serverHostName=&bootFileName=&routerAddress=192.168.1.1&useThisDnsServer=false&dnsServers=192.168.1.5&winsServers=192.168.1.5&ntpServers=192.168.1.5&staticRoutes=172.16.0.0|255.255.255.0|192.168.1.2&exclusions=192.168.1.1|192.168.1.10&reservedLeases=hostname|00-00-00-00-00-00|192.168.1.10|comments&allowOnlyReservedLeases=false` OBSOLETE PATH:\ `/api/setDhcpScope` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. - `newName` (optional): The new name of the DHCP scope to rename an existing scope. - `startingAddress` (optional): The starting IP address of the DHCP scope. This parameter is required when creating a new scope. - `endingAddress` (optional): The ending IP address of the DHCP scope. This parameter is required when creating a new scope. - `subnetMask` (optional): The subnet mask of the network. This parameter is required when creating a new scope. - `leaseTimeDays` (optional): The lease time in number of days. - `leaseTimeHours` (optional): The lease time in number of hours. - `leaseTimeMinutes` (optional): The lease time in number of minutes. - `offerDelayTime` (optional): The time duration in milliseconds that the DHCP server delays sending an DHCPOFFER message. - `pingCheckEnabled` (optional): Set this option to `true` to allow the DHCP server to find out if an IP address is already in use to prevent IP address conflict when some of the devices on the network have manually configured IP addresses. - `pingCheckTimeout` (optional): The timeout interval to wait for an ping reply. - `pingCheckRetries` (optional): The maximum number of ping requests to try. - `domainName` (optional): The domain name to be used by this network. The DHCP server automatically adds forward and reverse DNS entries for each IP address allocations when domain name is configured. (Option 15) - `domainSearchList` (optional): A comma separated list of domain names that the clients can use as a suffix when searching a domain name. (Option 119) - `dnsUpdates` (optional): Set this option to `true` to allow the DHCP server to automatically update forward and reverse DNS entries for clients. - `dnsOverwriteForDynamicLease` (optional): Set this option to `true` to allow the DHCP server to overwrite existing DNS A record matching the client domain name for dynamic leases. - `dnsTtl` (optional): The TTL value used for forward and reverse DNS records. - `serverAddress` (optional): The IP address of next server (TFTP) to use in bootstrap by the clients. If not specified, the DHCP server's IP address is used. (siaddr) - `serverHostName` (optional): The optional bootstrap server host name to be used by the clients to identify the TFTP server. (sname/Option 66) - `bootFileName` (optional): The boot file name stored on the bootstrap TFTP server to be used by the clients. (file/Option 67) - `routerAddress` (optional): The default gateway IP address to be used by the clients. (Option 3) - `useThisDnsServer` (optional): Tells the DHCP server to use this DNS server's IP address to configure the DNS Servers DHCP option for clients. - `dnsServers` (optional): A comma separated list of DNS server IP addresses to be used by the clients. This parameter is ignored when `useThisDnsServer` is set to `true`. (Option 6) - `winsServers` (optional): A comma separated list of NBNS/WINS server IP addresses to be used by the clients. (Option 44) - `ntpServers` (optional): A comma separated list of Network Time Protocol (NTP) server IP addresses to be used by the clients. (Option 42) - `ntpServerDomainNames` (optional): Enter NTP server domain names (e.g. pool.ntp.org) above that the DHCP server should automatically resolve and pass the resolved IP addresses to clients as NTP server option. (Option 42) - `staticRoutes` (optional): A `|` separated list of static routes in format `{destination network address}|{subnet mask}|{router/gateway address}` to be used by the clients for accessing specified destination networks. (Option 121) - `vendorInfo` (optional): A `|` separated list of vendor information in format `{vendor class identifier}|{vendor specific information}` where `{vendor specific information}` is a colon separated hex string or a normal hex string. - `capwapAcIpAddresses` (optional): A comma separated list of Control And Provisioning of Wireless Access Points (CAPWAP) Access Controller IP addresses to be used by Wireless Termination Points to discover the Access Controllers to which it is to connect. (Option 138) - `tftpServerAddresses` (optional): A comma separated list of TFTP Server Address or the VoIP Configuration Server Address. (Option 150) - `genericOptions` (optional): This feature allows you to define DHCP options that are not yet directly supported. Use a `|` separated list of DHCP option code defined for it and the value in either a colon (:) separated hex string or a normal hex string in format `{option-code}|{hex-string-value}`. - `exclusions` (optional): A `|` separated list of IP address range in format `{starting address}|{ending address}` that must be excluded or not assigned dynamically to any client by the DHCP server. - `reservedLeases` (optional): A `|` separated list of reserved IP addresses in format `{host name}|{MAC address}|{reserved IP address}|{comments}` to be assigned to specific clients based on their MAC address. - `allowOnlyReservedLeases` (optional): Set this parameter to `true` to stop dynamic IP address allocation and allocate only reserved IP addresses. - `blockLocallyAdministeredMacAddresses` (optional): Set this parameter to `true` to stop dynamic IP address allocation for clients with locally administered MAC addresses. MAC address with 0x02 bit set in the first octet indicate a locally administered MAC address which usually means that the device is not using its original MAC address. - `ignoreClientIdentifierOption` (optional): Set this parameter to `true` to always use the client's MAC address as the identifier to allocate lease instead of the Client Identifier (Option 61) provided by the client in the request. Changing this option may cause the existing clients to get a different IP lease on renewal. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Add Reserved Lease Adds a reserved lease entry to the specified scope. URL:\ `http://localhost:5380/api/dhcp/scopes/addReservedLease?token=x&name=Default&hardwareAddress=00:00:00:00:00:00&ipAddress=192.168.1.11` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. - `hardwareAddress`: The MAC address of the client. - `ipAddress`: The reserved IP address for the client. - `hostName` (optional): The hostname of the client to override. - `comments` (optional): Comments for the reserved lease entry. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Remove Reserved Lease Removed a reserved lease entry from the specified scope. URL:\ `http://localhost:5380/api/dhcp/scopes/removeReservedLease?token=x&name=Default&hardwareAddress=00:00:00:00:00:00` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. - `hardwareAddress`: The MAC address of the client. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Enable DHCP Scope Enables the DHCP scope allowing the server to allocate leases. URL:\ `http://localhost:5380/api/dhcp/scopes/enable?token=x&name=Default` OBSOLETE PATH:\ `/api/enableDhcpScope` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Disable DHCP Scope Disables the DHCP scope and stops any further lease allocations. URL:\ `http://localhost:5380/api/dhcp/scopes/disable?token=x&name=Default` OBSOLETE PATH:\ `/api/disableDhcpScope` PERMISSIONS:\ DhcpServer: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Delete DHCP Scope Permanently deletes the DHCP scope from the disk. URL:\ `http://localhost:5380/api/dhcp/scopes/delete?token=x&name=Default` OBSOLETE PATH:\ `/api/deleteDhcpScope` PERMISSIONS:\ DhcpServer: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `name`: The name of the DHCP scope. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ## Administration API Calls Allows managing the DNS server administration which includes managing all sessions, users, groups, and permissions. ### List Sessions Returns a list of active user sessions. URL:\ `http://localhost:5380/api/admin/sessions/list?token=x` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "sessions": [ { "username": "admin", "isCurrentSession": true, "partialToken": "272f4890427b9ab5", "type": "Standard", "tokenName": null, "lastSeen": "2022-09-17T13:23:44.9972772Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" }, { "username": "admin", "isCurrentSession": false, "partialToken": "ddfaecb8e9325e77", "type": "ApiToken", "tokenName": "MyToken1", "lastSeen": "2022-09-17T13:22:45.6710766Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" } ] }, "status": "ok" } ``` ### Create API Token Allows creating a non-expiring API token that can be used with automation scripts to make API calls. The token allows access to API calls with the same privileges as that of the user and thus its advised to create a separate user with limited permissions required for creating the API token. The token cannot be used to change the user's password, or update the user profile details. URL:\ `http://localhost:5380/api/admin/sessions/createToken?token=x&user=admin&tokenName=MyToken1` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `user`: The username for the user account for which to generate the API token. - `tokenName`: The name of the created token to identify its session. RESPONSE: ``` { "response": { "username": "admin", "tokenName": "MyToken1", "token": "ddfaecb8e9325e77865ee7e100f89596a65d3eae0e6dddcb33172355b95a64af" }, "status": "ok" } ``` ### Delete Session Deletes a specified user's session. URL:\ `http://localhost:5380/api/admin/sessions/delete?token=x&partialToken=ddfaecb8e9325e77` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `partialToken`: The partial token of the session to delete that was returned by the list of sessions. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### List Users Returns a list of all users. URL:\ `http://localhost:5380/api/admin/users/list?token=x` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "users": [ { "displayName": "Administrator", "username": "admin", "disabled": false, "previousSessionLoggedOn": "2022-09-17T13:20:32.7933783Z", "previousSessionRemoteAddress": "127.0.0.1", "recentSessionLoggedOn": "2022-09-17T13:22:45.671081Z", "recentSessionRemoteAddress": "127.0.0.1" }, { "displayName": "Shreyas Zare", "username": "shreyas", "disabled": false, "previousSessionLoggedOn": "0001-01-01T00:00:00Z", "previousSessionRemoteAddress": "0.0.0.0", "recentSessionLoggedOn": "0001-01-01T00:00:00Z", "recentSessionRemoteAddress": "0.0.0.0" } ] }, "status": "ok" } ``` ### Create User Creates a new user account. URL:\ `http://localhost:5380/api/admin/users/create?token=x&displayName=User&user=user1&pass=password` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `user`: A unique username for the user account. - `pass`: A password for the user account. - `displayName` (optional): The display name for the user account. RESPONSE: ``` { "response": { "displayName": "User", "username": "user1", "disabled": false, "previousSessionLoggedOn": "0001-01-01T00:00:00", "previousSessionRemoteAddress": "0.0.0.0", "recentSessionLoggedOn": "0001-01-01T00:00:00", "recentSessionRemoteAddress": "0.0.0.0" }, "status": "ok" } ``` ### Get User Details Returns a user account profile details. URL:\ `http://localhost:5380/api/admin/users/get?token=x&user=admin&includeGroups=true PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `user`: The username for the user account. - `includeGroups` (optional): Set `true` to include a list of groups in response. RESPONSE: ``` { "response": { "displayName": "Administrator", "username": "admin", "totpEnabled": false, "disabled": false, "previousSessionLoggedOn": "2022-09-16T13:22:45.671Z", "previousSessionRemoteAddress": "127.0.0.1", "recentSessionLoggedOn": "2022-09-18T09:55:26.9800695Z", "recentSessionRemoteAddress": "127.0.0.1", "sessionTimeoutSeconds": 1800, "memberOfGroups": [ "Administrators" ], "sessions": [ { "username": "admin", "isCurrentSession": false, "partialToken": "1f8011516cea27af", "type": "Standard", "tokenName": null, "lastSeen": "2022-09-18T09:55:40.6519988Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" }, { "username": "admin", "isCurrentSession": false, "partialToken": "ddfaecb8e9325e77", "type": "ApiToken", "tokenName": "MyToken1", "lastSeen": "2022-09-17T13:22:45.671Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" } ], "groups": [ "Administrators", "DHCP Administrators", "DNS Administrators" ] }, "status": "ok" } ``` ### Set User Details Allows changing user account profile details. URL:\ `http://localhost:5380/api/admin/users/set?token=x&user=admin&displayName=Administrator&disabled=false&sessionTimeoutSeconds=1800&memberOfGroups=Administrators` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `user`: The username for the user account. - `displayName` (optional): The display name for the user account. - `newUser` (optional): A new username for renaming the username for the user account. - `totpEnabled` (optional): Set to `false` to disable 2FA for the user account. The parameter cannot have a `true` value. - `disabled` (optional): Set `true` to disable the user account and delete all its active sessions. - `sessionTimeoutSeconds` (optional): A session time out value in seconds for the user account. - `newPass` (optional): A new password to reset the user account password. - `iterations` (optional): The number of iterations for PBKDF2 SHA256 password hashing. This is only used with the `newPass` option. - `memberOfGroups` (optional): A list of comma separated group names that the user must be set as a member. RESPONSE: ``` { "response": { "displayName": "Administrator", "username": "admin", "totpEnabled": false, "disabled": false, "previousSessionLoggedOn": "2022-09-17T13:22:45.671Z", "previousSessionRemoteAddress": "127.0.0.1", "recentSessionLoggedOn": "2022-09-18T09:55:26.9800695Z", "recentSessionRemoteAddress": "127.0.0.1", "sessionTimeoutSeconds": 1800, "memberOfGroups": [ "Administrators" ], "sessions": [ { "username": "admin", "isCurrentSession": false, "partialToken": "1f8011516cea27af", "type": "Standard", "tokenName": null, "lastSeen": "2022-09-18T09:59:19.9034491Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" }, { "username": "admin", "isCurrentSession": false, "partialToken": "ddfaecb8e9325e77", "type": "ApiToken", "tokenName": "MyToken1", "lastSeen": "2022-09-17T13:22:45.671Z", "lastSeenRemoteAddress": "127.0.0.1", "lastSeenUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" } ] }, "status": "ok" } ``` ### Delete User Deletes a user account. URL:\ `http://localhost:5380/api/admin/users/delete?token=x&user=user1` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `user`: The username for the user account to delete. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### List Groups Returns a list of all groups. URL:\ `http://localhost:5380/api/admin/groups/list?token=x` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "groups": [ { "name": "Administrators", "description": "Super administrators" }, { "name": "DHCP Administrators", "description": "DHCP service administrators" }, { "name": "DNS Administrators", "description": "DNS service administrators" } ] }, "status": "ok" } ``` ### Create Group Creates a new group. URL:\ `http://localhost:5380/api/admin/groups/create?token=x&group=Group1&description=My%20description` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `group`: The name of the group to create. - `description` (optional): The description text for the group. RESPONSE: ``` { "response": { "name": "Group1", "description": "My description" }, "status": "ok" } ``` ### Get Group Details Returns the details for a group. URL:\ `http://localhost:5380/api/admin/groups/get?token=x&group=Administrators&includeUsers=true` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `group`: The name of the group. - `includeUsers` (optional): Set `true` to include a list of users in response. RESPONSE: ``` { "response": { "name": "Administrators", "description": "Super administrators", "members": [ "admin" ], "users": [ "admin", "shreyas" ] }, "status": "ok" } ``` ### Set Group Details Allows changing group description or rename a group. URL:\ `http://localhost:5380/api/admin/groups/set?token=x&group=Administrators&description=Super%20administrators&members=admin` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `group`: The name of the group to update. - `newGroup` (optional): A new group name to rename the group. - `description` (optional): A new group description. - `members` (optional): A comma separated list of usernames to set as the group's members. RESPONSE: ``` { "response": { "name": "Administrators", "description": "Super administrators", "members": [ "admin" ] }, "status": "ok" } ``` ### Delete Group Allows deleting a group. URL:\ `http://localhost:5380/api/admin/groups/delete?token=x&group=Group1` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `group`: The name of the group to delete. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### List Permissions URL:\ `http://localhost:5380/api/admin/permissions/list?token=x` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "permissions": [ { "section": "Dashboard", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Zones", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DHCP Administrators", "canView": true, "canModify": false, "canDelete": false }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Cache", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Allowed", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Blocked", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Apps", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "DnsClient", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DHCP Administrators", "canView": true, "canModify": false, "canDelete": false }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Settings", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DNS Administrators", "canView": true, "canModify": true, "canDelete": true } ] }, { "section": "DhcpServer", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DHCP Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, { "section": "Administration", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true } ] }, { "section": "Logs", "userPermissions": [], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "DHCP Administrators", "canView": true, "canModify": false, "canDelete": false }, { "name": "DNS Administrators", "canView": true, "canModify": false, "canDelete": false }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] } ] }, "status": "ok" } ``` ### Get Permission Details Gets details of the permissions for the specified section. URL:\ `http://localhost:5380/api/admin/permissions/get?token=x§ion=Dashboard&includeUsersAndGroups=true` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `section`: The name of the section as given in the list of permissions API call. - `includeUsersAndGroups` (optional): Set to `true` to include a list of users and groups in the response. RESPONSE: ``` { "response": { "section": "Dashboard", "userPermissions": [ { "username": "shreyas", "canView": true, "canModify": false, "canDelete": false } ], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ], "users": [ "admin", "shreyas" ], "groups": [ "Administrators", "DHCP Administrators", "DNS Administrators", "Everyone" ] }, "status": "ok" } ``` ### Set Permission Details Allows changing permissions for the specified section. URL:\ `http://localhost:5380/api/admin/permissions/set?token=x§ion=Dashboard&userPermissions=shreyas|true|false|false&groupPermissions=Administrators|true|true|true|Everyone|true|false|false` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `section`: The name of the section as given in the list of permissions API call. - `userPermissions` (optional): A pipe `|` separated table data with each row containing username and boolean values for the view, modify and delete permissions. For example: user1|true|true|true|user2|true|false|false - `groupPermissions` (optional): A pipe `|` separated table data with each row containing the group name and boolean values for the view, modify and delete permissions. For example: group1|true|true|true|group2|true|true|false RESPONSE: ``` { "response": { "section": "Dashboard", "userPermissions": [ { "username": "shreyas", "canView": true, "canModify": false, "canDelete": false } ], "groupPermissions": [ { "name": "Administrators", "canView": true, "canModify": true, "canDelete": true }, { "name": "Everyone", "canView": true, "canModify": false, "canDelete": false } ] }, "status": "ok" } ``` ### Get Cluster state Returns data on the current state of the Cluster. URL:\ `http://localhost:5380/api/admin/cluster/state?token=x&includeServerIpAddresses=true` PERMISSIONS:\ Administration: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `includeServerIpAddresses` (optional): Set to `true` to return a list of static IP addresses configured on the server. Default value is `false` when the parameter is missing. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-26T12:30:16Z", "nodes": [ { "id": 1342079372, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddress": "192.168.10.5", "type": "Secondary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" }, { "id": 1653399468, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddress": "192.168.10.101", "type": "Secondary", "state": "Unreachable", "lastSeen": "0001-01-01T00:00:00" }, { "id": 1843286864, "name": "server3.example.com", "url": "https://server3.example.com:53443/", "ipAddress": "192.168.10.102", "type": "Primary", "state": "Connected", "lastSeen": "2025-09-26T12:30:16Z" } ], "serverIpAddresses": [ "192.168.10.5", "192.168.120.1", "192.168.180.1", ] }, "status": "ok" } ``` ### Initialize Cluster The initialization of a new Cluster will make the current DNS server its Primary node. You can add other DNS servers to this Cluster later which will get added as Secondary nodes. No data will be lost on this DNS server in this process. Note! If the web service does not have HTTPS enabled, then the initialization process will enable it automatically with a self-signed certificate. However, its recommended to manually configure HTTPS with a valid certificate before initializing the cluster. Note! The initialization process will create two zones if they do not exist. The first zone will be the Cluster Primary zone named as the Cluster domain name specified above. The second zone will be the Cluster Catalog zone that uses 'cluster-catalog' as the subdomain name of the Cluster domain name. Use this Cluster Catalog zone for automatic provisioning of Secondary zones on all of the Cluster Secondary nodes. Warning! The Cluster domain name cannot be changed later. Make sure that you enter the correct domain name before proceeding. URL:\ `http://localhost:5380/api/admin/cluster/init?token=x&clusterDomain=example.com&primaryNodeIpAddresses=192.168.10.5` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `clusterDomain`: The fully qualified domain name to be used to identify the new Cluster. - `primaryNodeIpAddresses`: A comma separated list of IP addresses of this DNS server that will be accessible by all other DNS Servers to be added later as Secondary nodes. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "nodes": [ { "id": 1081800048, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Primary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Delete Cluster The Delete Cluster process will remove all Cluster configuration from this Primary node. There will be no data loss except for the Cluster configuration. You will need to re-initialize the Cluster again to use clustering features on this DNS server. This call can be made only at the Primary node. Note! You can delete the Cluster only when there are no Secondary nodes in the Cluster. Use the Force Delete Cluster option only when you wish this Primary node to be removed from the Cluster even when there are Secondary nodes in the Cluster. In this case, the Secondary nodes will become orphaned and you will need to promote one of them to be the new Primary node manually. URL:\ `http://localhost:5380/api/admin/cluster/primary/delete?token=x&forceDelete=false` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `forceDelete` (optional): Set to `true` to cause this Primary node to delete the Cluster for itself even when other Secondary nodes still exist, orphaning them. Default value is `false` when the parameter is missing. RESPONSE: ``` { "response": { "clusterInitialized": false, "dnsServerDomain": "server1", "version": "14.0" }, "status": "ok" } ``` ### Join Cluster This API call is used by a DNS Server instance to ask the Primary node to allow it to join the Cluster as a Secondary node. This call can be only at the Primary node. URL:\ `http://localhost:5380/api/admin/cluster/primary/join?token=x` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `secondaryNodeId`: The Secondary node ID that was generated randomly when the server is initializing as a Secondary node. - `secondaryNodeUrl`: The HTTPS URL of the Secondary node's API web service. - `secondaryNodeIpAddresses`: A comma separated list of Secondary node IP addresses. - `secondaryNodeCertificate`: The Secondary node's TLS certificate used by the API web service. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-26T12:30:16Z", "nodes": [ { "id": 1342079372, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Secondary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" }, { "id": 1653399468, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Secondary", "state": "Unreachable", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Remove Secondary Node The Remove Node process will ask the selected Secondary node to leave the Cluster gracefully. The Secondary now will then initiate Leave Cluster process as if the Leave Cluster action was performed on that node itself. This call can be made only at the Primary node. URL:\ `http://localhost:5380/api/admin/cluster/primary/removeSecondary?token=X&secondaryNodeId=811905692` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `secondaryNodeId`: The Secondary node ID which needs to be asked to leave the Cluster. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "nodes": [ { "id": 1151850285, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Primary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Delete Secondary Node The Delete Node process will immediately delete the selected Secondary node entry from the Cluster without asking the node to leave graceful. This call can be made only at the Primary node. URL:\ `http://localhost:5380/api/admin/cluster/primary/deleteSecondary?token=X&secondaryNodeId=811905692` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `secondaryNodeId`: The Secondary node ID which must be deleted from the Cluster immediately. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "nodes": [ { "id": 1151850285, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Primary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Update Secondary Node The Update Secondary Node call is used by the Secondary node to update its details like URL, IP Address and TLS certificate on the Primary node. This call can be made only at the Primary node. URL:\ `http://localhost:5380/api/admin/cluster/primary/updateSecondary?token=x&secondaryNodeId=811905692&secondaryNodeUrl=https%3A%2F%2Fserver2.example.com%3A53443%2F&secondaryNodeIpAddresses=192.168.10.101&secondaryNodeCertificate=` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `secondaryNodeId`: The Secondary node ID that identifies the node. - `secondaryNodeUrl`: The HTTPS API web service URL of the Secondary node to be updated. - `secondaryNodeIpAddresses`: A comma separated list of IP addresses of the Secondary node to be updated. - `secondaryNodeCertificate`: The web service TLS certificate encoded in Base64 URL format. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-26T12:30:16Z", "nodes": [ { "id": 1342079372, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Secondary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" }, { "id": 1653399468, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Secondary", "state": "Connected", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Transfer Config The Transfer Config call is used by Secondary nodes to sync the complete config data from the Primary node. This call can be made only at the Primary node. URL:\ `http://localhost:5380/api/admin/cluster/primary/transferConfig?token=x&includeZones=` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `includeZones` (optional): A list of comma separated domain names of the zones that should be included to transfer DNSSEC private keys. RESPONSE HEADERS:\ - `If-Modified-Since` (optional): The date time stamp of the last config transfer so as to allow transferring only changes done after the specified date time. The format is the standard HTTP header format for `If-Modified-Since`. RESPONSE: A zip file with content type `application/zip` and content disposition set to `attachment`. ### Set Cluster Options Allows setting Cluster options to be used by all Secondary nodes. This call can be made only at the Primary node. URL:\ `http://localhost:5380/api/admin/cluster/primary/setOptions?token=x` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `heartbeatRefreshIntervalSeconds` (optional): The interval in seconds in which the DNS server must refresh the state of all nodes in the Cluster. The valid range is `10`-`300` and default value is `30`. - `heartbeatRetryIntervalSeconds` (optional): The interval in seconds in which the DNS server must retry the state refresh process for all nodes in case of a failure. The valid range is `10`-`300` and default value is `10`. - `configRefreshIntervalSeconds` (optional): The interval in seconds in which the DNS server must refresh the configuration from the Primary node. The valid range is `30`-`3600` and default value is `900`. - `configRetryIntervalSeconds` (optional): The interval in seconds in which the DNS server must retry the configuration refresh process for the Primary node in case of a failure. The valid range is `30`-`3600` and default value is `60`. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server1.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-26T12:30:16Z", "nodes": [ { "id": 1342079372, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Secondary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" }, { "id": 1653399468, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Secondary", "state": "Connected", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Initialize And Join Cluster Joining a Cluster will make this DNS server its Secondary node. This process will overwrite configuration on this DNS server for Allowed, Blocked, Apps, Settings and Administration sections. The DNS server will automatically synchronize its configuration with the Primary node in the Cluster. This call can be only at the Secondary node. Note! The process to join the Cluster may take a while to complete depending on the amount of initial config data that needs to be synchronized from the Primary node. Please be patient till the process completes. Note! If the web service does not have HTTPS enabled, then the joining process will enable it automatically with a self-signed certificate. However, its recommended to manually configure HTTPS with a valid certificate before joining the cluster. Warning! Joining a Cluster will cause configuration on this DNS server to be overwritten permanently for Allowed, Blocked, Apps, Settings and Administration sections! URL:\ `http://server2.home:5380/api/admin/cluster/initJoin?token=x&secondaryNodeIpAddresses=192.168.10.101&primaryNodeUrl=https%3A%2F%2Fserver1.example.com%3A53443%2F&primaryNodeIpAddress=192.168.10.5&ignoreCertificateErrors=true&primaryNodeUsername=admin&primaryNodePassword=admin&primaryNodeTotp=` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `secondaryNodeIpAddresses`: A comma separated list of IP addresses of this DNS server that will be accessible by all other DNS Server nodes in the Cluster. - `primaryNodeUrl`: The web service HTTPS URL of the Primary node in the Cluster. - `primaryNodeIpAddress` (optional): The IP address of the Primary node in the Cluster. When unspecified, domain name in the Primary node URL will be resolved and used. - `ignoreCertificateErrors` (optional): Set to `true` only when you know that the Primary node web service is using a self-signed TLS certificate and is reachable on a private network. - `primaryNodeUsername`: The username of an administrator on the Primary node in the Cluster. - `primaryNodePassword`: The password of the administrator user specified above. - `primaryNodeTotp` (optional): The the 6-digit code you see in your authenticator app for the administrator user specified above. Only to be used if the user has 2FA enabled. REQUEST: This is a `POST` request call where the content type of the request must be `application/x-www-form-urlencoded`. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server2.example.com", "version": "14.3", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-27T13:19:55Z", "nodes": [ { "id": 1151850285, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Primary", "state": "Connected", "lastSeen": "2025-09-27T13:19:54.6215569Z" }, { "id": 811905692, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Secondary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Leave Cluster The Leave Cluster process will remove all Cluster configuration from this Secondary node and leave the Cluster gracefully. There will be no data loss except for the Cluster configuration. You will need to re-join the Cluster again to use this DNS server as a Secondary node. This call can be made only at the Secondary node. Note! Use the Force Leave Cluster option only when the Primary node is unreachable/decommissioned and thus cannot leave the Cluster gracefully. URL:\ `http://localhost:5380/api/admin/cluster/secondary/leave?token=x` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `forceLeave` (optional): Set to `true` to make this Secondary node to leave the Cluster without informing the Primary node. RESPONSE: ``` { "response": { "clusterInitialized": false, "dnsServerDomain": "server2", "version": "14.0" }, "status": "ok" } ``` ### Notify The Notify call is made by the Primary node to all Secondary nodes in the Cluster whenever there is any change in the configuration that the Secondary zones must sync to. The Secondary nodes have to use the Transfer Config call to sync the config changes when this notification is received. The Notify call also includes the latest details of the Primary node that must be updated and used. This call can be made only at the Secondary node. URL:\ `http://localhost:5380/api/admin/cluster/secondary/notify?token=x` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `primaryNodeId`: The calling Primary node's ID to allow identification at the Secondary node. - `primaryNodeUrl`: The calling Primary node's API web service URL. - `primaryNodeIpAddresses`: A comma separated list of the calling Primary node's IP addresses. RESPONSE: ``` { "status": "ok" } ``` ### Resync Cluster The Resync cluster call allows an admin user to manually trigger a complete configuration resync on the Secondary node. When triggered, the Secondary node will use the Transfer Config call to sync the complete configuration from the Primary node. This call can be made only at the Secondary node. URL:\ `http://localhost:5380/api/admin/cluster/secondary/resync?token=x` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "status": "ok" } ``` ### Update Primary Node Allows updating the Primary node details like the API URL and IP address on the Secondary node. This call is useful when the Primary node's IP address or URL has changed while the Secondary node was offline causing it to have old details of the Primary node. This call can be made only at the Secondary node. URL:\ `http://localhost:5380/api/admin/cluster/secondary/updatePrimary?token=x` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `primaryNodeUrl`: The web service HTTPS URL of the Primary node in the Cluster. - `primaryNodeIpAddresses` (optional): The IP address of the Primary node in the Cluster. When unspecified, domain name in the Primary node URL will be resolved and used. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server2.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-27T13:19:55Z", "nodes": [ { "id": 1151850285, "name": "server1.example.com", "url": "https://server1.example.com:53443/", "ipAddresses": [ "192.168.10.5" ], "type": "Primary", "state": "Connected", "lastSeen": "2025-09-27T13:19:54.6215569Z" }, { "id": 811905692, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Secondary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Promote To Primary The promote To Primary node process will resync complete configuration from the Primary node and then proceed to delete it from the Cluster followed by upgrading the selected Secondary node to become the Primary node in the Cluster. The former Primary node when deleted will cause it to delete all its own Cluster configuration leaving the Cluster without causing any other data loss. This call can be made only at the Secondary node. Note! Use the Force Delete Current Primary Node option only when the Primary node is unreachable/decommissioned and thus cannot be deleted from the Cluster gracefully. Note! The process to promote to Primary node may take a while to complete depending on the size of the complete configuration being resynced and the number of local zones that need to be converted. Please be patient till the process completes. URL:\ `http://localhost:5380/api/admin/cluster/secondary/promote?token=x` PERMISSIONS:\ Administration: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `forceDeletePrimary` (optional): Set to `true` for the current Primary node to be deleted from the Cluster without resyncing complete configuration from it and without inform it. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server2.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-27T13:19:55Z", "nodes": [ { "id": 811905692, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Primary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ### Update Node IP Addresses Allows to update the current Cluster node's IP address. This call can be made at both the Primary and Secondary nodes. URL:\ `http://localhost:5380/api/admin/cluster/updateIpAddresses?token=x` PERMISSIONS:\ Administration: Modify WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `ipAddresses`: A comma separated list of IP addresses to be updated for the current node. RESPONSE: ``` { "response": { "clusterInitialized": true, "dnsServerDomain": "server2.example.com", "version": "14.0", "clusterDomain": "example.com", "heartbeatRefreshIntervalSeconds": 30, "heartbeatRetryIntervalSeconds": 10, "configRefreshIntervalSeconds": 900, "configRetryIntervalSeconds": 60, "configLastSynced": "2025-09-27T13:19:55Z", "nodes": [ { "id": 811905692, "name": "server2.example.com", "url": "https://server2.example.com:53443/", "ipAddresses": [ "192.168.10.101" ], "type": "Primary", "state": "Self", "lastSeen": "0001-01-01T00:00:00" } ] }, "status": "ok" } ``` ## Log API Calls ### List Logs Lists all logs files available on the DNS server. URL:\ `http://localhost:5380/api/logs/list?token=x` OBSOLETE PATH:\ `/api/listLogs` PERMISSIONS:\ Logs: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": { "logFiles": [ { "fileName": "2020-09-19", "size": "8.14 KB" }, { "fileName": "2020-09-15", "size": "5.6 KB" }, { "fileName": "2020-09-12", "size": "18.4 KB" }, { "fileName": "2020-09-11", "size": "1.78 KB" }, { "fileName": "2020-09-10", "size": "2.03 KB" } ] }, "status": "ok" } ``` ### Download Log Downloads the log file. URL:\ `http://localhost:5380/api/logs/download?token=x&fileName=2020-09-10&limit=2` OBSOLETE PATH:\ `/log/{fileName}` PERMISSIONS:\ Logs: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `fileName`: The `fileName` returned by the List Logs API call. - `limit` (optional): The limit of number of mega bytes to download the log file. Default value is `0` when parameter is missing which indicates there is no limit. RESPONSE: Response is a downloadable file with `Content-Type: text/plain` and `Content-Disposition: attachment;filename=name` ### Delete Log Permanently deletes a log file from the disk. URL: `http://localhost:5380/api/logs/delete?token=x&log=2020-09-19` OBSOLETE PATH:\ `/api/deleteLog` PERMISSIONS:\ Logs: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `log`: The `fileName` returned by the List Logs API call. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Delete All Logs Permanently delete all log files from the disk. URL:\ `http://localhost:5380/api/logs/deleteAll?token=x` OBSOLETE PATH:\ `/api/deleteAllLogs` PERMISSIONS:\ Logs: Delete WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. RESPONSE: ``` { "response": {}, "status": "ok" } ``` ### Query Logs Queries for logs to a specified DNS app. URL:\ `http://localhost:5380/api/logs/query?token=x&name=AppName&classPath=AppClassPath&=pageNumber=1&entriesPerPage=10&descendingOrder=true&start=yyyy-MM-dd HH:mm:ss&end=yyyy-MM-dd HH:mm:ss&clientIpAddress=&protocol=&responseType=&rcode=&qname=&qtype=&qclass=` OBSOLETE PATH:\ `/api/queryLogs` PERMISSIONS:\ Logs: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `name`: The name of the installed DNS app. - `classPath`: The class path of the DNS app. - `pageNumber` (optional): The page number of the data set to retrieve. - `entriesPerPage` (optional): The number of entries per page. - `descendingOrder` (optional): Orders the selected data set in descending order. - `start` (optional): The start date time in ISO 8601 format to filter the logs. - `end` (optional): The end date time in ISO 8601 format to filter the logs. - `clientIpAddress` (optional): The client IP address to filter the logs. - `protocol` (optional): The DNS transport protocol to filter the logs. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. - `responseType` (optional): The DNS server response type to filter the logs. Valid values are [`Authoritative`, `Recursive`, `Cached`, `Blocked`, `UpstreamBlocked`, `CacheBlocked`]. - `rcode` (optional): The DNS response code to filter the logs. - `qname` (optional): The query name (QNAME) in the request question section to filter the logs. - `qtype` (optional): The DNS resource record type (QTYPE) in the request question section to filter the logs. - `qclass` (optional): The DNS class (QCLASS) in the request question section to filter the logs. RESPONSE: ``` { "response": { "pageNumber": 1, "totalPages": 2, "totalEntries": 13, "entries": [ { "rowNumber": 1, "timestamp": "2021-09-10T12:22:52Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Recursive", "responseRtt": 33.45, "rcode": "NoError", "qname": "google.com", "qtype": "A", "qclass": "IN", "answer": "172.217.166.46" }, { "rowNumber": 2, "timestamp": "2021-09-10T12:37:02Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Blocked", "rcode": "NxDomain", "qname": "example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 3, "timestamp": "2021-09-11T09:13:31Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Authoritative", "rcode": "ServerFailure", "qname": "example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 4, "timestamp": "2021-09-11T09:14:48Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Authoritative", "rcode": "ServerFailure", "qname": "example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 5, "timestamp": "2021-09-11T09:27:25Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Blocked", "rcode": "NxDomain", "qname": "example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 6, "timestamp": "2021-09-11T09:27:29Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Blocked", "rcode": "NxDomain", "qname": "www.example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 7, "timestamp": "2021-09-11T09:28:36Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Blocked", "rcode": "NxDomain", "qname": "www.example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 8, "timestamp": "2021-09-11T09:28:41Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Blocked", "rcode": "NxDomain", "qname": "example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 9, "timestamp": "2021-09-11T09:28:44Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Blocked", "rcode": "NxDomain", "qname": "sdfsdf.example.com", "qtype": "A", "qclass": "IN", "answer": "" }, { "rowNumber": 10, "timestamp": "2021-09-11T09:42:02Z", "clientIpAddress": "127.0.0.1", "protocol": "Udp", "responseType": "Recursive", "responseRtt": 23.63, "rcode": "NoError", "qname": "technitium.com", "qtype": "A", "qclass": "IN", "answer": "139.59.3.235" } ] }, "status": "ok" } ``` ### Export Query Logs Queries for logs to a specified DNS app and exports the data as a CSV file. URL:\ `http://localhost:5380/api/logs/export?token=x&name=AppName&classPath=AppClassPath&start=yyyy-MM-dd HH:mm:ss&end=yyyy-MM-dd HH:mm:ss&clientIpAddress=&protocol=&responseType=&rcode=&qname=&qtype=&qclass=` PERMISSIONS:\ Logs: View WHERE: - `token`: The session token generated by the `login` or the `createToken` call. - `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized. - `name`: The name of the installed DNS app. - `classPath`: The class path of the DNS app. - `start` (optional): The start date time in ISO 8601 format to filter the logs. - `end` (optional): The end date time in ISO 8601 format to filter the logs. - `clientIpAddress` (optional): The client IP address to filter the logs. - `protocol` (optional): The DNS transport protocol to filter the logs. Valid values are [`Udp`, `Tcp`, `Tls`, `Https`, `Quic`]. - `responseType` (optional): The DNS server response type to filter the logs. Valid values are [`Authoritative`, `Recursive`, `Cached`, `Blocked`, `UpstreamBlocked`, `CacheBlocked`]. - `rcode` (optional): The DNS response code to filter the logs. - `qname` (optional): The query name (QNAME) in the request question section to filter the logs. - `qtype` (optional): The DNS resource record type (QTYPE) in the request question section to filter the logs. - `qclass` (optional): The DNS class (QCLASS) in the request question section to filter the logs. RESPONSE: Response is a downloadable text file with `Content-Type: text/csv` and `Content-Disposition: attachment` headers set. ================================================ FILE: Apps/AdvancedBlockingApp/AdvancedBlockingApp.csproj ================================================  net9.0 false 10.0 false Technitium Technitium DNS Server Shreyas Zare AdvancedBlockingApp AdvancedBlocking https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Blocks domain names using block lists and regex block lists. Supports creating groups based on client's IP address or subnet to enforce different block lists and regex block lists for each group.\n\nNote! This app works independent of the DNS server's built-in blocking feature. The options configured in DNS server Settings section does not apply to this app. false Library enable false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/AdvancedBlockingApp/App.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Http.Client; namespace AdvancedBlocking { public sealed class App : IDnsApplication, IDnsRequestBlockingHandler { #region variables IDnsServer? _dnsServer; DnsSOARecordData? _soaRecord; DnsNSRecordData? _nsRecord; bool _enableBlocking; uint _blockingAnswerTtl; int _blockListUrlUpdateIntervalHours; int _blockListUrlUpdateIntervalMinutes; Dictionary? _localEndPointGroupMap; Dictionary? _networkGroupMap; Dictionary? _groups; Dictionary _allAllowListZones = []; Dictionary _allBlockListZones = []; Dictionary _allRegexAllowListZones = []; Dictionary _allRegexBlockListZones = []; Dictionary _allAdBlockListZones = []; Timer? _blockListUrlUpdateTimer; DateTime _blockListUrlLastUpdatedOn; const int BLOCK_LIST_UPDATE_TIMER_INTERVAL = 60000; #endregion #region IDisposable public void Dispose() { if (_blockListUrlUpdateTimer is not null) { _blockListUrlUpdateTimer.Dispose(); _blockListUrlUpdateTimer = null; } } #endregion #region private private async void BlockListUrlUpdateTimerCallbackAsync(object? state) { try { if (DateTime.UtcNow > _blockListUrlLastUpdatedOn.AddHours(_blockListUrlUpdateIntervalHours).AddMinutes(_blockListUrlUpdateIntervalMinutes)) { if (await UpdateAllListsAsync()) { //block lists were updated //save last updated on time _blockListUrlLastUpdatedOn = DateTime.UtcNow; } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } } private async Task UpdateAllListsAsync() { List> updateTasks = new List>(); foreach (KeyValuePair allAllowListZone in _allAllowListZones) updateTasks.Add(allAllowListZone.Value.UpdateAsync()); foreach (KeyValuePair allBlockListZone in _allBlockListZones) updateTasks.Add(allBlockListZone.Value.UpdateAsync()); foreach (KeyValuePair allRegexAllowListZone in _allRegexAllowListZones) updateTasks.Add(allRegexAllowListZone.Value.UpdateAsync()); foreach (KeyValuePair allRegexBlockListZone in _allRegexBlockListZones) updateTasks.Add(allRegexBlockListZone.Value.UpdateAsync()); foreach (KeyValuePair allAdBlockListZone in _allAdBlockListZones) updateTasks.Add(allAdBlockListZone.Value.UpdateAsync()); await Task.WhenAll(updateTasks); foreach (Task updateTask in updateTasks) { bool downloaded = await updateTask; if (downloaded) return true; } return false; } private static string? GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private static bool IsZoneFound(HashSet domains, string domain, out string? foundZone) { do { if (domains.Contains(domain)) { foundZone = domain; return true; } domain = GetParentZone(domain)!; } while (domain is not null); foundZone = null; return false; } private static bool IsZoneFound(Dictionary listZones, string domain, out string? foundZone, out Uri? listUri) { foreach (KeyValuePair listZone in listZones) { if (listZone.Value.IsZoneFound(domain, out foundZone)) { listUri = listZone.Key; return true; } } foundZone = null; listUri = null; return false; } private static bool IsZoneFound(Dictionary> listZones, string domain, out string? foundZone, out UrlEntry? listUri) { foreach (KeyValuePair> listZone in listZones) { if (listZone.Value.List.IsZoneFound(domain, out foundZone)) { listUri = listZone.Value.UrlEntry; return true; } } foundZone = null; listUri = null; return false; } private static bool IsZoneAllowed(Dictionary> listZones, string domain, out string? foundZone, out UrlEntry? listUri) { foreach (KeyValuePair> listZone in listZones) { if (listZone.Value.List.IsZoneAllowed(domain, out foundZone)) { listUri = listZone.Value.UrlEntry; return true; } } foundZone = null; listUri = null; return false; } private static bool IsZoneBlocked(Dictionary> listZones, string domain, out string? foundZone, out UrlEntry? listUri) { foreach (KeyValuePair> listZone in listZones) { if (listZone.Value.List.IsZoneBlocked(domain, out foundZone)) { listUri = listZone.Value.UrlEntry; return true; } } foundZone = null; listUri = null; return false; } private static bool IsMatchFound(IReadOnlyList regices, string domain, out string? matchingPattern) { foreach (Regex regex in regices) { if (regex.IsMatch(domain)) { //found pattern matchingPattern = regex.ToString(); return true; } } matchingPattern = null; return false; } private static bool IsMatchFound(Dictionary regexListZones, string domain, out string? matchingPattern, out Uri? listUri) { foreach (KeyValuePair regexListZone in regexListZones) { if (regexListZone.Value.IsMatchFound(domain, out matchingPattern)) { listUri = regexListZone.Key; return true; } } matchingPattern = null; listUri = null; return false; } private static bool IsMatchFound(Dictionary> regexListZones, string domain, out string? matchingPattern, out UrlEntry? listUri) { foreach (KeyValuePair> regexListZone in regexListZones) { if (regexListZone.Value.List.IsMatchFound(domain, out matchingPattern)) { listUri = regexListZone.Value.UrlEntry; return true; } } matchingPattern = null; listUri = null; return false; } private string? GetGroupName(DnsDatagram request, IPEndPoint remoteEP) { if ((request.Metadata is not null) && (request.Metadata.NameServer is not null)) { Uri requestLocalUriEP = request.Metadata.NameServer.DoHEndPoint; if (requestLocalUriEP is not null) { foreach (KeyValuePair entry in _localEndPointGroupMap!) { if (entry.Key is DomainEndPoint ep) { if (((ep.Port == 0) || (ep.Port == requestLocalUriEP.Port)) && ep.Address.Equals(requestLocalUriEP.Host, StringComparison.OrdinalIgnoreCase)) return entry.Value; } } } DomainEndPoint requestLocalDomainEP = request.Metadata.NameServer.DomainEndPoint; if (requestLocalDomainEP is not null) { foreach (KeyValuePair entry in _localEndPointGroupMap!) { if (entry.Key is DomainEndPoint ep) { if (((ep.Port == 0) || (ep.Port == requestLocalDomainEP.Port)) && ep.Address.Equals(requestLocalDomainEP.Address, StringComparison.OrdinalIgnoreCase)) return entry.Value; } } } IPEndPoint requestLocalEP = request.Metadata.NameServer.IPEndPoint; if (requestLocalEP is not null) { foreach (KeyValuePair entry in _localEndPointGroupMap!) { if (entry.Key is IPEndPoint ep) { if (((ep.Port == 0) || (ep.Port == requestLocalEP.Port)) && ep.Address.Equals(requestLocalEP.Address)) return entry.Value; } } } } string? groupName = null; IPAddress remoteIP = remoteEP.Address; NetworkAddress? network = null; foreach (KeyValuePair entry in _networkGroupMap!) { if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) { network = entry.Key; groupName = entry.Value; } } return groupName; } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; Directory.CreateDirectory(Path.Combine(_dnsServer.ApplicationFolder, "blocklists")); using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _enableBlocking = jsonConfig.GetPropertyValue("enableBlocking", true); _blockingAnswerTtl = jsonConfig.GetPropertyValue("blockingAnswerTtl", 30u); _blockListUrlUpdateIntervalHours = jsonConfig.GetPropertyValue("blockListUrlUpdateIntervalHours", 24); _blockListUrlUpdateIntervalMinutes = jsonConfig.GetPropertyValue("blockListUrlUpdateIntervalMinutes", 0); _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, _blockingAnswerTtl); _nsRecord = new DnsNSRecordData(_dnsServer.ServerDomain); if (jsonConfig.TryReadObjectAsMap("localEndPointGroupMap", delegate (string localEP, JsonElement jsonGroup) { if (!EndPointExtensions.TryParse(localEP, out EndPoint ep)) throw new InvalidOperationException("Local end point group map contains an invalid end point: " + localEP); return new Tuple(ep, jsonGroup.GetString() ?? ""); }, out Dictionary localEndPointGroupMap)) { _localEndPointGroupMap = localEndPointGroupMap; } _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string network, JsonElement jsonGroup) { if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) throw new InvalidOperationException("Network group map contains an invalid network address: " + network); return new Tuple(networkAddress, jsonGroup.GetString() ?? ""); }); { Dictionary allAllowListZones = new Dictionary(0); Dictionary allBlockListZones = new Dictionary(0); Dictionary allRegexAllowListZones = new Dictionary(0); Dictionary allRegexBlockListZones = new Dictionary(0); Dictionary allAdBlockListZones = new Dictionary(0); _groups = jsonConfig.ReadArrayAsMap("groups", delegate (JsonElement jsonGroup) { Group group = new Group(this, jsonGroup); foreach (Uri allowListUrl in group.AllowListUrls) { if (!allAllowListZones.ContainsKey(allowListUrl)) { if (_allAllowListZones.TryGetValue(allowListUrl, out BlockList? allowList)) allAllowListZones.Add(allowListUrl, allowList); else allAllowListZones.Add(allowListUrl, new BlockList(_dnsServer, allowListUrl, true)); } } foreach (UrlEntry blockListUrl in group.BlockListUrls) { if (!allBlockListZones.ContainsKey(blockListUrl.Uri!)) { if (_allBlockListZones.TryGetValue(blockListUrl.Uri!, out BlockList? blockList)) allBlockListZones.Add(blockListUrl.Uri!, blockList); else allBlockListZones.Add(blockListUrl.Uri!, new BlockList(_dnsServer, blockListUrl.Uri!, false)); } } foreach (Uri regexAllowListUrl in group.RegexAllowListUrls) { if (!allRegexAllowListZones.ContainsKey(regexAllowListUrl)) { if (_allRegexAllowListZones.TryGetValue(regexAllowListUrl, out RegexList? regexAllowList)) allRegexAllowListZones.Add(regexAllowListUrl, regexAllowList); else allRegexAllowListZones.Add(regexAllowListUrl, new RegexList(_dnsServer, regexAllowListUrl, true)); } } foreach (UrlEntry regexBlockListUrl in group.RegexBlockListUrls) { if (!allRegexBlockListZones.ContainsKey(regexBlockListUrl.Uri!)) { if (_allRegexBlockListZones.TryGetValue(regexBlockListUrl.Uri!, out RegexList? regexBlockList)) allRegexBlockListZones.Add(regexBlockListUrl.Uri!, regexBlockList); else allRegexBlockListZones.Add(regexBlockListUrl.Uri!, new RegexList(_dnsServer, regexBlockListUrl.Uri!, false)); } } foreach (UrlEntry adblockListUrl in group.AdblockListUrls) { if (!allAdBlockListZones.ContainsKey(adblockListUrl.Uri!)) { if (_allAdBlockListZones.TryGetValue(adblockListUrl.Uri!, out AdBlockList? adBlockList)) allAdBlockListZones.Add(adblockListUrl.Uri!, adBlockList); else allAdBlockListZones.Add(adblockListUrl.Uri!, new AdBlockList(_dnsServer, adblockListUrl.Uri!)); } } return new Tuple(group.Name, group); }); _allAllowListZones = allAllowListZones; _allBlockListZones = allBlockListZones; _allRegexAllowListZones = allRegexAllowListZones; _allRegexBlockListZones = allRegexBlockListZones; _allAdBlockListZones = allAdBlockListZones; } foreach (KeyValuePair group in _groups) { group.Value.LoadListZones(); _dnsServer.WriteLog("Advanced Blocking app loaded all zones successfully for group: " + group.Key); } ThreadPool.QueueUserWorkItem(async delegate (object? state) { try { List loadTasks = new List(); foreach (KeyValuePair allAllowListZone in _allAllowListZones) loadTasks.Add(allAllowListZone.Value.LoadAsync()); foreach (KeyValuePair allBlockListZone in _allBlockListZones) loadTasks.Add(allBlockListZone.Value.LoadAsync()); foreach (KeyValuePair allRegexAllowListZone in _allRegexAllowListZones) loadTasks.Add(allRegexAllowListZone.Value.LoadAsync()); foreach (KeyValuePair allRegexBlockListZone in _allRegexBlockListZones) loadTasks.Add(allRegexBlockListZone.Value.LoadAsync()); foreach (KeyValuePair allAdBlockListZone in _allAdBlockListZones) loadTasks.Add(allAdBlockListZone.Value.LoadAsync()); await Task.WhenAll(loadTasks); if (_blockListUrlUpdateTimer is null) { DateTime latest = DateTime.MinValue; foreach (KeyValuePair allAllowListZone in _allAllowListZones) { if (allAllowListZone.Value.LastModified > latest) latest = allAllowListZone.Value.LastModified; } foreach (KeyValuePair allBlockListZone in _allBlockListZones) { if (allBlockListZone.Value.LastModified > latest) latest = allBlockListZone.Value.LastModified; } foreach (KeyValuePair allRegexAllowListZone in _allRegexAllowListZones) { if (allRegexAllowListZone.Value.LastModified > latest) latest = allRegexAllowListZone.Value.LastModified; } foreach (KeyValuePair allRegexBlockListZone in _allRegexBlockListZones) { if (allRegexBlockListZone.Value.LastModified > latest) latest = allRegexBlockListZone.Value.LastModified; } foreach (KeyValuePair allAdBlockListZone in _allAdBlockListZones) { if (allAdBlockListZone.Value.LastModified > latest) latest = allAdBlockListZone.Value.LastModified; } _blockListUrlLastUpdatedOn = latest; _blockListUrlUpdateTimer = new Timer(BlockListUrlUpdateTimerCallbackAsync, null, Timeout.Infinite, Timeout.Infinite); _blockListUrlUpdateTimer.Change(BLOCK_LIST_UPDATE_TIMER_INTERVAL, BLOCK_LIST_UPDATE_TIMER_INTERVAL); } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } }); if (!jsonConfig.TryGetProperty("localEndPointGroupMap", out _)) { config = config.Replace("\"networkGroupMap\"", "\"localEndPointGroupMap\": {\r\n },\r\n \"networkGroupMap\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } if (!jsonConfig.TryGetProperty("blockingAnswerTtl", out _)) { config = config.Replace("\"blockListUrlUpdateIntervalHours\"", "\"blockingAnswerTtl\": 30,\r\n \"blockListUrlUpdateIntervalHours\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } if (!jsonConfig.TryGetProperty("blockListUrlUpdateIntervalMinutes", out _)) { config = config.Replace("\"localEndPointGroupMap\"", "\"blockListUrlUpdateIntervalMinutes\": 0,\r\n \"localEndPointGroupMap\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } } public Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP) { if (!_enableBlocking) return Task.FromResult(false); string? groupName = GetGroupName(request, remoteEP); if ((groupName is null) || !_groups!.TryGetValue(groupName, out Group? group) || !group.EnableBlocking) return Task.FromResult(false); DnsQuestionRecord question = request.Question[0]; return Task.FromResult(group.IsZoneAllowed(question.Name)); } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP) { if (!_enableBlocking) return Task.FromResult(null); string? groupName = GetGroupName(request, remoteEP); if ((groupName is null) || !_groups!.TryGetValue(groupName, out Group? group) || !group.EnableBlocking) return Task.FromResult(null); DnsQuestionRecord question = request.Question[0]; if (!group.IsZoneBlocked(question.Name, out string? blockedDomain, out string? blockedRegex, out UrlEntry? blockListUrl)) return Task.FromResult(null); string GetBlockingReport() { string blockingReport = "source=advanced-blocking-app; group=" + group.Name; if (blockedRegex is null) { if (blockListUrl!.Uri is not null) blockingReport += "; blockListUrl=" + blockListUrl.Uri.AbsoluteUri + "; domain=" + blockedDomain; else blockingReport += "; domain=" + blockedDomain; } else { if (blockListUrl!.Uri is not null) blockingReport += "; regexBlockListUrl=" + blockListUrl.Uri.AbsoluteUri + "; regex=" + blockedRegex; else blockingReport += "; regex=" + blockedRegex; } return blockingReport; } if (group.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) { //return meta data string blockingReport = GetBlockingReport(); DnsResourceRecord[] answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _blockingAnswerTtl, new DnsTXTRecordData(blockingReport))]; return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer)); } else { EDnsOption[]? options = null; if (group.AllowTxtBlockingReport && (request.EDNS is not null)) { string blockingReport = GetBlockingReport(); options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, blockingReport))]; } DnsResponseCode rcode; IReadOnlyList? answer = null; IReadOnlyList? authority = null; if (blockListUrl!.BlockAsNxDomain) { rcode = DnsResponseCode.NxDomain; if (blockedDomain is null) blockedDomain = question.Name; string? parentDomain = GetParentZone(blockedDomain); if (parentDomain is null) parentDomain = string.Empty; authority = [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)]; } else { rcode = DnsResponseCode.NoError; switch (question.Type) { case DnsResourceRecordType.A: { List rrList = new List(blockListUrl.ARecords.Count); foreach (DnsARecordData record in blockListUrl.ARecords) rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _blockingAnswerTtl, record)); answer = rrList; } break; case DnsResourceRecordType.AAAA: { List rrList = new List(blockListUrl.AAAARecords.Count); foreach (DnsAAAARecordData record in blockListUrl.AAAARecords) rrList.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _blockingAnswerTtl, record)); answer = rrList; } break; case DnsResourceRecordType.NS: if (blockedDomain is null) blockedDomain = question.Name; if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase)) answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, _blockingAnswerTtl, _nsRecord)]; else authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)]; break; case DnsResourceRecordType.SOA: if (blockedDomain is null) blockedDomain = question.Name; answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)]; break; default: if (blockedDomain is null) blockedDomain = question.Name; authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _soaRecord)]; break; } } return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, rcode, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer!.UdpPayloadSize, EDnsHeaderFlags.None, options)); } } #endregion #region properties public string Description { get { return "Blocks domain names using block lists and regex block lists. Supports creating groups based on client's IP address or subnet to enforce different block lists and regex block lists for each group."; } } #endregion class UrlEntry { #region variables readonly Uri? _uri; readonly bool _blockAsNxDomain; readonly List _aRecords; readonly List _aaaaRecords; #endregion #region constructor public UrlEntry(Uri? uri, Group group) { _uri = uri; _blockAsNxDomain = group.BlockAsNxDomain; _aRecords = group.ARecords; _aaaaRecords = group.AAAARecords; } public UrlEntry(JsonElement jsonUrl, Group group) { switch (jsonUrl.ValueKind) { case JsonValueKind.String: _uri = new Uri(jsonUrl.GetString()!); _blockAsNxDomain = group.BlockAsNxDomain; _aRecords = group.ARecords; _aaaaRecords = group.AAAARecords; break; case JsonValueKind.Object: _uri = new Uri(jsonUrl.GetProperty("url").GetString()!); if (jsonUrl.TryGetProperty("blockAsNxDomain", out JsonElement jsonBlockAsNxDomain)) _blockAsNxDomain = jsonBlockAsNxDomain.GetBoolean(); else _blockAsNxDomain = group.BlockAsNxDomain; if (jsonUrl.TryGetProperty("blockingAddresses", out JsonElement jsonBlockingAddresses)) { List aRecords = new List(); List aaaaRecords = new List(); foreach (JsonElement jsonBlockingAddress in jsonBlockingAddresses.EnumerateArray()) { string? strAddress = jsonBlockingAddress.GetString(); if (IPAddress.TryParse(strAddress, out IPAddress? address)) { switch (address.AddressFamily) { case AddressFamily.InterNetwork: aRecords.Add(new DnsARecordData(address)); break; case AddressFamily.InterNetworkV6: aaaaRecords.Add(new DnsAAAARecordData(address)); break; } } } _aRecords = aRecords.Count > 0 ? aRecords : group.ARecords; _aaaaRecords = aaaaRecords.Count > 0 ? aaaaRecords : group.AAAARecords; } else { _aRecords = group.ARecords; _aaaaRecords = group.AAAARecords; } break; default: throw new InvalidDataException("Unexpected URL format: " + jsonUrl.ValueKind); } } #endregion #region properties public Uri? Uri { get { return _uri; } } public bool BlockAsNxDomain { get { return _blockAsNxDomain; } } public List ARecords { get { return _aRecords; } } public List AAAARecords { get { return _aaaaRecords; } } #endregion } class ListZoneEntry where T : ListBase { #region variables readonly UrlEntry _urlEntry; readonly T _list; #endregion #region constructor public ListZoneEntry(UrlEntry urlEntry, T list) { _urlEntry = urlEntry; _list = list; } #endregion #region public public UrlEntry UrlEntry { get { return _urlEntry; } } public T List { get { return _list; } } #endregion } class Group { #region variables readonly App _app; readonly string _name; readonly bool _enableBlocking; readonly bool _allowTxtBlockingReport; readonly bool _blockAsNxDomain; readonly List _aRecords; readonly List _aaaaRecords; readonly HashSet _allowed; readonly HashSet _blocked; readonly Uri[] _allowListUrls; readonly UrlEntry[] _blockListUrls; readonly Regex[] _allowedRegex; readonly Regex[] _blockedRegex; readonly Uri[] _regexAllowListUrls; readonly UrlEntry[] _regexBlockListUrls; readonly UrlEntry[] _adblockListUrls; Dictionary _allowListZones = []; Dictionary> _blockListZones = []; Dictionary _regexAllowListZones = []; Dictionary> _regexBlockListZones = []; Dictionary> _adBlockListZones = []; #endregion #region constructor public Group(App app, JsonElement jsonGroup) { _app = app; _name = jsonGroup.GetProperty("name").GetString()!; _enableBlocking = jsonGroup.GetPropertyValue("enableBlocking", true); _allowTxtBlockingReport = jsonGroup.GetPropertyValue("allowTxtBlockingReport", true); _blockAsNxDomain = jsonGroup.GetPropertyValue("blockAsNxDomain", false); if (jsonGroup.TryGetProperty("blockingAddresses", out JsonElement jsonBlockingAddresses)) { List aRecords = new List(); List aaaaRecords = new List(); foreach (JsonElement jsonBlockingAddress in jsonBlockingAddresses.EnumerateArray()) { string? strAddress = jsonBlockingAddress.GetString(); if (IPAddress.TryParse(strAddress, out IPAddress? address)) { switch (address.AddressFamily) { case AddressFamily.InterNetwork: aRecords.Add(new DnsARecordData(address)); break; case AddressFamily.InterNetworkV6: aaaaRecords.Add(new DnsAAAARecordData(address)); break; } } } _aRecords = aRecords; _aaaaRecords = aaaaRecords; } else { _aRecords = []; _aaaaRecords = []; } _allowed = jsonGroup.ReadArrayAsSet("allowed"); _blocked = jsonGroup.ReadArrayAsSet("blocked"); _allowListUrls = jsonGroup.ReadArray("allowListUrls", GetUriEntry); _blockListUrls = jsonGroup.ReadArray("blockListUrls", GetUrlEntry); _allowedRegex = jsonGroup.ReadArray("allowedRegex", GetRegexEntry); _blockedRegex = jsonGroup.ReadArray("blockedRegex", GetRegexEntry); _regexAllowListUrls = jsonGroup.ReadArray("regexAllowListUrls", GetUriEntry); _regexBlockListUrls = jsonGroup.ReadArray("regexBlockListUrls", GetUrlEntry); _adblockListUrls = jsonGroup.ReadArray("adblockListUrls", GetUrlEntry); } #endregion #region private private static Uri GetUriEntry(string uriString) { return new Uri(uriString); } private UrlEntry GetUrlEntry(JsonElement jsonUrl) { return new UrlEntry(jsonUrl, this); } private static Regex GetRegexEntry(string pattern) { return new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); } #endregion #region public public void LoadListZones() { { Dictionary allowListZones = new Dictionary(_allowListUrls.Length); foreach (Uri listUrl in _allowListUrls) { if (_app._allAllowListZones.TryGetValue(listUrl, out BlockList? allowListZone)) allowListZones.Add(listUrl, allowListZone); } _allowListZones = allowListZones; } { Dictionary> blockListZones = new Dictionary>(_blockListUrls.Length); foreach (UrlEntry listUrl in _blockListUrls) { if (_app._allBlockListZones.TryGetValue(listUrl.Uri!, out BlockList? blockListZone)) blockListZones.Add(listUrl.Uri!, new ListZoneEntry(listUrl, blockListZone)); } _blockListZones = blockListZones; } { Dictionary regexAllowListZones = new Dictionary(_regexAllowListUrls.Length); foreach (Uri listUrl in _regexAllowListUrls) { if (_app._allRegexAllowListZones.TryGetValue(listUrl, out RegexList? regexAllowListZone)) regexAllowListZones.Add(listUrl, regexAllowListZone); } _regexAllowListZones = regexAllowListZones; } { Dictionary> regexBlockListZones = new Dictionary>(_regexBlockListUrls.Length); foreach (UrlEntry listUrl in _regexBlockListUrls) { if (_app._allRegexBlockListZones.TryGetValue(listUrl.Uri!, out RegexList? regexBlockListZone)) regexBlockListZones.Add(listUrl.Uri!, new ListZoneEntry(listUrl, regexBlockListZone)); } _regexBlockListZones = regexBlockListZones; } { Dictionary> adBlockListZones = new Dictionary>(_adblockListUrls.Length); foreach (UrlEntry listUrl in _adblockListUrls) { if (_app._allAdBlockListZones.TryGetValue(listUrl.Uri!, out AdBlockList? adBlockListZone)) adBlockListZones.Add(listUrl.Uri!, new ListZoneEntry(listUrl, adBlockListZone)); } _adBlockListZones = adBlockListZones; } } public bool IsZoneAllowed(string domain) { domain = domain.ToLowerInvariant(); //allowed, allow list zone, allowedRegex, regex allow list zone, adblock list zone return IsZoneFound(_allowed, domain, out _) || IsZoneFound(_allowListZones, domain, out _, out _) || IsMatchFound(_allowedRegex, domain, out _) || IsMatchFound(_regexAllowListZones, domain, out _, out _) || App.IsZoneAllowed(_adBlockListZones, domain, out _, out _); } public bool IsZoneBlocked(string domain, out string? blockedDomain, out string? blockedRegex, out UrlEntry? listUrl) { domain = domain.ToLowerInvariant(); //blocked if (IsZoneFound(_blocked, domain, out string? foundZone1)) { //found zone blocked blockedDomain = foundZone1; blockedRegex = null; listUrl = new UrlEntry(null, this); return true; } //block list zone if (IsZoneFound(_blockListZones, domain, out string? foundZone2, out UrlEntry? blockListUrl1)) { //found zone blocked blockedDomain = foundZone2; blockedRegex = null; listUrl = blockListUrl1; return true; } //blockedRegex if (IsMatchFound(_blockedRegex, domain, out string? blockedPattern1)) { //found pattern blocked blockedDomain = null; blockedRegex = blockedPattern1; listUrl = new UrlEntry(null, this); return true; } //regex block list zone if (IsMatchFound(_regexBlockListZones, domain, out string? blockedPattern2, out UrlEntry? blockListUrl2)) { //found pattern blocked blockedDomain = null; blockedRegex = blockedPattern2; listUrl = blockListUrl2; return true; } //adblock list zone if (App.IsZoneBlocked(_adBlockListZones, domain, out string? foundZone3, out UrlEntry? blockListUrl3)) { //found zone blocked blockedDomain = foundZone3; blockedRegex = null; listUrl = blockListUrl3; return true; } blockedDomain = null; blockedRegex = null; listUrl = null; return false; } #endregion #region properties public string Name { get { return _name; } } public bool EnableBlocking { get { return _enableBlocking; } } public bool AllowTxtBlockingReport { get { return _allowTxtBlockingReport; } } public bool BlockAsNxDomain { get { return _blockAsNxDomain; } } public List ARecords { get { return _aRecords; } } public List AAAARecords { get { return _aaaaRecords; } } public Uri[] AllowListUrls { get { return _allowListUrls; } } public UrlEntry[] BlockListUrls { get { return _blockListUrls; } } public UrlEntry[] RegexBlockListUrls { get { return _regexBlockListUrls; } } public Uri[] RegexAllowListUrls { get { return _regexAllowListUrls; } } public UrlEntry[] AdblockListUrls { get { return _adblockListUrls; } } #endregion } abstract class ListBase { #region variables protected readonly IDnsServer _dnsServer; protected readonly Uri _listUrl; protected readonly bool _isAllowList; protected readonly bool _isRegexList; protected readonly bool _isAdblockList; protected readonly string _listFilePath; bool _listZoneLoaded; DateTime _lastModified; volatile bool _isLoading; #endregion #region constructor public ListBase(IDnsServer dnsServer, Uri listUrl, bool isAllowList, bool isRegexList, bool isAdblockList) { _dnsServer = dnsServer; _listUrl = listUrl; _isAllowList = isAllowList; _isRegexList = isRegexList; _isAdblockList = isAdblockList; _listFilePath = Path.Combine(Path.Combine(_dnsServer.ApplicationFolder, "blocklists"), Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(_listUrl.AbsoluteUri))).ToLowerInvariant()); } #endregion #region private private async Task DownloadListFileAsync() { try { _dnsServer.WriteLog("Advanced Blocking app is downloading " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list: " + _listUrl.AbsoluteUri); if (_listUrl.IsFile) { if (File.Exists(_listFilePath)) { if (File.GetLastWriteTimeUtc(_listUrl.LocalPath) <= File.GetLastWriteTimeUtc(_listFilePath)) { _dnsServer.WriteLog("Advanced Blocking app successfully checked for a new update of the " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list: " + _listUrl.AbsoluteUri); return false; } } File.Copy(_listUrl.LocalPath, _listFilePath, true); _lastModified = File.GetLastWriteTimeUtc(_listFilePath); _dnsServer.WriteLog("Advanced Blocking app successfully downloaded " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list (" + WebUtilities.GetFormattedSize(new FileInfo(_listFilePath).Length) + "): " + _listUrl.AbsoluteUri); return true; } else { HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsServer.Proxy; handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsServer; using (HttpClient http = new HttpClient(handler)) { if (File.Exists(_listFilePath)) http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(_listFilePath); HttpResponseMessage httpResponse = await http.GetAsync(_listUrl); switch (httpResponse.StatusCode) { case HttpStatusCode.OK: string listDownloadFilePath = _listFilePath + ".downloading"; using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write)) { using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync()) { await httpStream.CopyToAsync(fS); } } File.Move(listDownloadFilePath, _listFilePath, true); if (httpResponse.Content.Headers.LastModified is null) { _lastModified = DateTime.UtcNow; } else { _lastModified = httpResponse.Content.Headers.LastModified.Value.UtcDateTime; File.SetLastWriteTimeUtc(_listFilePath, _lastModified); } _dnsServer.WriteLog("Advanced Blocking app successfully downloaded " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list (" + WebUtilities.GetFormattedSize(new FileInfo(_listFilePath).Length) + "): " + _listUrl.AbsoluteUri); return true; case HttpStatusCode.NotModified: _dnsServer.WriteLog("Advanced Blocking app successfully checked for a new update of the " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list: " + _listUrl.AbsoluteUri); return false; default: throw new HttpRequestException((int)httpResponse.StatusCode + " " + httpResponse.ReasonPhrase); } } } } catch (Exception ex) { _dnsServer.WriteLog("Advanced Blocking app failed to download " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list and will use previously downloaded file (if available): " + _listUrl.AbsoluteUri + "\r\n" + ex.ToString()); return false; } } #endregion #region protected protected abstract void LoadListZone(); #endregion #region public public async Task LoadAsync() { if (_isLoading) return; _isLoading = true; try { if (File.Exists(_listFilePath)) { _lastModified = File.GetLastWriteTimeUtc(_listFilePath); if (_listUrl.IsFile && (File.GetLastWriteTimeUtc(_listUrl.LocalPath) > _lastModified)) { File.Copy(_listUrl.LocalPath, _listFilePath, true); _lastModified = File.GetLastWriteTimeUtc(_listFilePath); _dnsServer.WriteLog("Advanced Blocking app successfully downloaded " + (_isAdblockList ? "adblock" : (_isRegexList ? "regex " : "") + (_isAllowList ? "allow" : "block")) + " list (" + WebUtilities.GetFormattedSize(new FileInfo(_listFilePath).Length) + "): " + _listUrl.AbsoluteUri); LoadListZone(); _listZoneLoaded = true; } else if (!_listZoneLoaded) { LoadListZone(); _listZoneLoaded = true; } } else { if (await DownloadListFileAsync()) { LoadListZone(); _listZoneLoaded = true; } } } finally { _isLoading = false; } } public async Task UpdateAsync() { if (await DownloadListFileAsync()) { LoadListZone(); return true; } return false; } #endregion #region properties public DateTime LastModified { get { return _lastModified; } } #endregion } class BlockList : ListBase { #region variables readonly static char[] _popWordSeperator = new char[] { ' ', '\t' }; HashSet _listZone = []; #endregion #region constructor public BlockList(IDnsServer dnsServer, Uri listUrl, bool isAllowList) : base(dnsServer, listUrl, isAllowList, false, false) { } #endregion #region private private static string PopWord(ref string line) { if (line.Length == 0) return line; line = line.TrimStart(_popWordSeperator); int i = line.IndexOfAny(_popWordSeperator); string word; if (i < 0) { word = line; line = ""; } else { word = line.Substring(0, i); line = line.Substring(i + 1); } return word; } private Queue ReadListFile() { Queue domains = new Queue(); try { _dnsServer.WriteLog("Advanced Blocking app is reading " + (_isAllowList ? "allow" : "block") + " list from: " + _listUrl.AbsoluteUri); using (FileStream fS = new FileStream(_listFilePath, FileMode.Open, FileAccess.Read)) { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); char[] trimSeperator = new char[] { ' ', '\t', '*', '.' }; string? line; string firstWord; string secondWord; string hostname; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('#')) continue; //skip comment line firstWord = PopWord(ref line); if (line.Length == 0) { hostname = firstWord; } else { secondWord = PopWord(ref line); if ((secondWord.Length == 0) || secondWord.StartsWith('#')) hostname = firstWord; else hostname = secondWord; } hostname = hostname.Trim('.').ToLowerInvariant(); switch (hostname) { case "": case "localhost": case "localhost.localdomain": case "local": case "broadcasthost": case "ip6-localhost": case "ip6-loopback": case "ip6-localnet": case "ip6-mcastprefix": case "ip6-allnodes": case "ip6-allrouters": case "ip6-allhosts": continue; //skip these hostnames } if (!DnsClient.IsDomainNameValid(hostname)) continue; if (IPAddress.TryParse(hostname, out _)) continue; //skip line when hostname is IP address domains.Enqueue(hostname); } } _dnsServer.WriteLog("Advanced Blocking app read " + (_isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domains) from: " + _listUrl.AbsoluteUri); } catch (Exception ex) { _dnsServer.WriteLog("Advanced Blocking app failed to read " + (_isAllowList ? "allow" : "block") + " list from: " + _listUrl.AbsoluteUri + "\r\n" + ex.ToString()); } return domains; } #endregion #region protected protected override void LoadListZone() { Queue listQueue = ReadListFile(); HashSet listZone = new HashSet(listQueue.Count); while (listQueue.Count > 0) listZone.Add(listQueue.Dequeue()); _listZone = listZone; } #endregion #region public public bool IsZoneFound(string domain, out string? foundZone) { return App.IsZoneFound(_listZone, domain, out foundZone); } #endregion } class RegexList : ListBase { #region variables IReadOnlyList _regexListZone = []; #endregion #region constructor public RegexList(IDnsServer dnsServer, Uri listUrl, bool isAllowList) : base(dnsServer, listUrl, isAllowList, true, false) { } #endregion #region private private Queue ReadRegexListFile() { Queue regices = new Queue(); try { _dnsServer.WriteLog("Advanced Blocking app is reading regex " + (_isAllowList ? "allow" : "block") + " list from: " + _listUrl.AbsoluteUri); using (FileStream fS = new FileStream(_listFilePath, FileMode.Open, FileAccess.Read)) { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); char[] trimSeperator = new char[] { ' ', '\t' }; string? line; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('#')) continue; //skip comment line regices.Enqueue(line); } } _dnsServer.WriteLog("Advanced Blocking app read regex " + (_isAllowList ? "allow" : "block") + " list file (" + regices.Count + " regex patterns) from: " + _listUrl.AbsoluteUri); } catch (Exception ex) { _dnsServer.WriteLog("Advanced Blocking app failed to read regex " + (_isAllowList ? "allow" : "block") + " list from: " + _listUrl.AbsoluteUri + "\r\n" + ex.ToString()); } return regices; } #endregion #region protected protected override void LoadListZone() { Queue regexPatterns = ReadRegexListFile(); List regexListZone = new List(regexPatterns.Count); while (regexPatterns.Count > 0) { try { regexListZone.Add(new Regex(regexPatterns.Dequeue(), RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)); } catch (RegexParseException ex) { _dnsServer.WriteLog(ex); } } _regexListZone = regexListZone; } #endregion #region public public bool IsMatchFound(string domain, out string? matchingPattern) { return App.IsMatchFound(_regexListZone, domain, out matchingPattern); } #endregion } class AdBlockList : ListBase { #region variables HashSet _allowedListZone = []; HashSet _blockedListZone = []; #endregion #region constructor public AdBlockList(IDnsServer dnsServer, Uri listUrl) : base(dnsServer, listUrl, false, false, true) { } #endregion #region private private void ReadAdblockListFile(out Queue allowedDomains, out Queue blockedDomains) { allowedDomains = new Queue(); blockedDomains = new Queue(); try { _dnsServer.WriteLog("Advanced Blocking app is reading adblock list from: " + _listUrl.AbsoluteUri); using (FileStream fS = new FileStream(_listFilePath, FileMode.Open, FileAccess.Read)) { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); char[] trimSeperator = new char[] { ' ', '\t' }; string? line; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('!')) continue; //skip comment line if (line.StartsWith("||")) { int i = line.IndexOf('^'); if (i > -1) { string domain = line.Substring(2, i - 2); string options = line.Substring(i + 1); if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) blockedDomains.Enqueue(domain); } else { string domain = line.Substring(2); if (DnsClient.IsDomainNameValid(domain)) blockedDomains.Enqueue(domain); } } else if (line.StartsWith("@@||")) { int i = line.IndexOf('^'); if (i > -1) { string domain = line.Substring(4, i - 4); string options = line.Substring(i + 1); if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) allowedDomains.Enqueue(domain); } else { string domain = line.Substring(4); if (DnsClient.IsDomainNameValid(domain)) allowedDomains.Enqueue(domain); } } } } _dnsServer.WriteLog("Advanced Blocking app read adblock list file (" + (allowedDomains.Count + blockedDomains.Count) + " domains) from: " + _listUrl.AbsoluteUri); } catch (Exception ex) { _dnsServer.WriteLog("Advanced Blocking app failed to read adblock list from: " + _listUrl.AbsoluteUri + "\r\n" + ex.ToString()); } } #endregion #region protected protected override void LoadListZone() { ReadAdblockListFile(out Queue allowedDomains, out Queue blockedDomains); HashSet allowedListZone = new HashSet(allowedDomains.Count); HashSet blockedListZone = new HashSet(blockedDomains.Count); while (allowedDomains.Count > 0) allowedListZone.Add(allowedDomains.Dequeue()); while (blockedDomains.Count > 0) blockedListZone.Add(blockedDomains.Dequeue()); _allowedListZone = allowedListZone; _blockedListZone = blockedListZone; } #endregion #region public public bool IsZoneAllowed(string domain, out string? foundZone) { return IsZoneFound(_allowedListZone, domain, out foundZone); } public bool IsZoneBlocked(string domain, out string? foundZone) { return IsZoneFound(_blockedListZone, domain, out foundZone); } #endregion } } } ================================================ FILE: Apps/AdvancedBlockingApp/dnsApp.config ================================================ { "enableBlocking": true, "blockingAnswerTtl": 30, "blockListUrlUpdateIntervalHours": 24, "blockListUrlUpdateIntervalMinutes": 0, "localEndPointGroupMap": { "127.0.0.1": "bypass", "192.168.10.2:53": "bypass", "user1.dot.example.com": "kids", "user2.doh.example.com:443": "bypass" }, "networkGroupMap": { "192.168.10.20": "kids", "0.0.0.0/0": "everyone", "[::]/0": "everyone" }, "groups": [ { "name": "everyone", "enableBlocking": true, "allowTxtBlockingReport": true, "blockAsNxDomain": true, "blockingAddresses": [ "0.0.0.0", "::" ], "allowed": [], "blocked": [ "example.com" ], "allowListUrls": [], "blockListUrls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" ], "allowedRegex": [], "blockedRegex": [ "^ads\\." ], "regexAllowListUrls": [], "regexBlockListUrls": [], "adblockListUrls": [] }, { "name": "kids", "enableBlocking": true, "allowTxtBlockingReport": true, "blockAsNxDomain": true, "blockingAddresses": [ "0.0.0.0", "::" ], "allowed": [], "blocked": [], "allowListUrls": [], "blockListUrls": [ { "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts", "blockAsNxDomain": false, "blockingAddresses": [ "192.168.10.2" ] } ], "allowedRegex": [], "blockedRegex": [], "regexAllowListUrls": [], "regexBlockListUrls": [], "adblockListUrls": [] }, { "name": "bypass", "enableBlocking": true, "allowTxtBlockingReport": true, "blockAsNxDomain": true, "blockingAddresses": [ "0.0.0.0", "::" ], "allowed": [], "blocked": [], "allowListUrls": [], "blockListUrls": [], "allowedRegex": [], "blockedRegex": [], "regexAllowListUrls": [], "regexBlockListUrls": [], "adblockListUrls": [] } ] } ================================================ FILE: Apps/AdvancedForwardingApp/AdvancedForwardingApp.csproj ================================================  net9.0 false 4.0 false Technitium Technitium DNS Server Shreyas Zare AdvancedForwardingApp AdvancedForwarding https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Provides advanced, bulk conditional forwarding options. Supports creating groups based on client's IP address or subnet to enable different conditional forwarding configuration for each group. Supports AdGuard Upstreams config files.\n\nNote: This app works independent of the DNS server's built-in Conditional Forwarder Zones feature. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest PreserveNewest ================================================ FILE: Apps/AdvancedForwardingApp/App.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace AdvancedForwarding { public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference { #region variables IDnsServer _dnsServer; byte _appPreference; bool _enableForwarding; Dictionary _configProxyServers; Dictionary _configForwarders; Dictionary _networkGroupMap; Dictionary _groups; #endregion #region IDisposable public void Dispose() { if (_groups is not null) { foreach (KeyValuePair group in _groups) group.Value.Dispose(); } } #endregion #region private private static List GetUpdatedForwarderRecords(IReadOnlyList forwarderRecords, bool dnssecValidation, ConfigProxyServer configProxyServer) { List newForwarderRecords = new List(forwarderRecords.Count); foreach (DnsForwarderRecordData forwarderRecord in forwarderRecords) newForwarderRecords.Add(GetForwarderRecord(forwarderRecord.Protocol, forwarderRecord.Forwarder, dnssecValidation, configProxyServer)); return newForwarderRecords; } private static DnsForwarderRecordData GetForwarderRecord(NameServerAddress forwarder, bool dnssecValidation, ConfigProxyServer configProxyServer) { return GetForwarderRecord(forwarder.Protocol, forwarder.ToString(), dnssecValidation, configProxyServer); } private static DnsForwarderRecordData GetForwarderRecord(DnsTransportProtocol protocol, string forwarder, bool dnssecValidation, ConfigProxyServer configProxyServer) { DnsForwarderRecordData forwarderRecord; if (configProxyServer is null) forwarderRecord = new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, DnsForwarderRecordProxyType.DefaultProxy, null, 0, null, null, 0); else forwarderRecord = new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, configProxyServer.Type, configProxyServer.ProxyAddress, configProxyServer.ProxyPort, configProxyServer.ProxyUsername, configProxyServer.ProxyPassword, 0); return forwarderRecord; } private Tuple ReadGroup(JsonElement jsonGroup) { string name = jsonGroup.GetProperty("name").GetString(); if ((_groups is not null) && _groups.TryGetValue(name, out Group group)) group.ReloadConfig(_configProxyServers, _configForwarders, jsonGroup); else group = new Group(_dnsServer, _configProxyServers, _configForwarders, jsonGroup); return new Tuple(group.Name, group); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue("appPreference", 200)); _enableForwarding = jsonConfig.GetPropertyValue("enableForwarding", true); if (jsonConfig.TryReadArrayAsMap("proxyServers", delegate (JsonElement jsonProxy) { ConfigProxyServer proxyServer = new ConfigProxyServer(jsonProxy); return new Tuple(proxyServer.Name, proxyServer); }, out Dictionary configProxyServers)) _configProxyServers = configProxyServers; else _configProxyServers = null; if (jsonConfig.TryReadArrayAsMap("forwarders", delegate (JsonElement jsonForwarder) { ConfigForwarder forwarder = new ConfigForwarder(jsonForwarder, _configProxyServers); return new Tuple(forwarder.Name, forwarder); }, out Dictionary configForwarders)) _configForwarders = configForwarders; else _configForwarders = null; _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string network, JsonElement jsonGroup) { if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) throw new FormatException("Network group map contains an invalid network address: " + network); return new Tuple(networkAddress, jsonGroup.GetString()); }); if (jsonConfig.TryReadArrayAsMap("groups", ReadGroup, out Dictionary groups)) { if (_groups is not null) { foreach (KeyValuePair group in _groups) { if (!groups.ContainsKey(group.Key)) group.Value.Dispose(); } } _groups = groups; } else { throw new FormatException("Groups array was not defined."); } return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { if (!_enableForwarding || !request.RecursionDesired) return Task.FromResult(null); IPAddress remoteIP = remoteEP.Address; NetworkAddress network = null; string groupName = null; foreach (KeyValuePair entry in _networkGroupMap) { if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) { network = entry.Key; groupName = entry.Value; } } if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableForwarding) return Task.FromResult(null); DnsQuestionRecord question = request.Question[0]; string qname = question.Name; if (!group.TryGetForwarderRecords(qname, out IReadOnlyList forwarderRecords)) return Task.FromResult(null); request.SetShadowEDnsClientSubnetOption(network, true); DnsResourceRecord[] authority = new DnsResourceRecord[forwarderRecords.Count]; for (int i = 0; i < forwarderRecords.Count; i++) authority[i] = new DnsResourceRecord(qname, DnsResourceRecordType.FWD, DnsClass.IN, 0, forwarderRecords[i]); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, authority)); } #endregion #region properties public string Description { get { return "Performs bulk conditional forwarding for configured domain names and AdGuard Upstream config files."; } } public byte Preference { get { return _appPreference; } } #endregion class Group : IDisposable { #region variables readonly IDnsServer _dnsServer; Dictionary _configProxyServers; Dictionary _configForwarders; readonly string _name; bool _enableForwarding; Forwarding[] _forwardings; Dictionary _adguardUpstreams; #endregion #region constructor public Group(IDnsServer dnsServer, Dictionary configProxyServers, Dictionary configForwarders, JsonElement jsonGroup) { _dnsServer = dnsServer; _name = jsonGroup.GetProperty("name").GetString(); ReloadConfig(configProxyServers, configForwarders, jsonGroup); } #endregion #region IDisposable public void Dispose() { if (_adguardUpstreams is not null) { foreach (KeyValuePair adguardUpstream in _adguardUpstreams) adguardUpstream.Value.Dispose(); _adguardUpstreams = null; } } #endregion #region private private Tuple ReadAdGuardUpstream(JsonElement jsonAdguardUpstream) { string name = jsonAdguardUpstream.GetProperty("configFile").GetString(); if ((_adguardUpstreams is not null) && _adguardUpstreams.TryGetValue(name, out AdGuardUpstream adGuardUpstream)) adGuardUpstream.ReloadConfig(_configProxyServers, jsonAdguardUpstream); else adGuardUpstream = new AdGuardUpstream(_dnsServer, _configProxyServers, jsonAdguardUpstream); return new Tuple(adGuardUpstream.Name, adGuardUpstream); } #endregion #region public public void ReloadConfig(Dictionary configProxyServers, Dictionary configForwarders, JsonElement jsonGroup) { _configProxyServers = configProxyServers; _configForwarders = configForwarders; _enableForwarding = jsonGroup.GetPropertyValue("enableForwarding", true); if (jsonGroup.TryReadArray("forwardings", delegate (JsonElement jsonForwarding) { return new Forwarding(jsonForwarding, _configForwarders); }, out Forwarding[] forwardings)) _forwardings = forwardings; else _forwardings = null; if (jsonGroup.TryReadArrayAsMap("adguardUpstreams", ReadAdGuardUpstream, out Dictionary adguardUpstreams)) { if (_adguardUpstreams is not null) { foreach (KeyValuePair adguardUpstream in _adguardUpstreams) { if (!adguardUpstreams.ContainsKey(adguardUpstream.Key)) adguardUpstream.Value.Dispose(); } } _adguardUpstreams = adguardUpstreams; } else { if (_adguardUpstreams is not null) { foreach (KeyValuePair adguardUpstream in _adguardUpstreams) adguardUpstream.Value.Dispose(); } _adguardUpstreams = null; } } public bool TryGetForwarderRecords(string domain, out IReadOnlyList forwarderRecords) { domain = domain.ToLowerInvariant(); if ((_forwardings is not null) && (_forwardings.Length > 0) && Forwarding.TryGetForwarderRecords(domain, _forwardings, out forwarderRecords)) return true; if (_adguardUpstreams is not null) { foreach (KeyValuePair adguardUpstream in _adguardUpstreams) { if (adguardUpstream.Value.TryGetForwarderRecords(domain, out forwarderRecords)) return true; } } forwarderRecords = null; return false; } #endregion #region properties public string Name { get { return _name; } } public bool EnableForwarding { get { return _enableForwarding; } } #endregion } class Forwarding { #region variables IReadOnlyList _forwarderRecords; readonly Dictionary _domainMap; #endregion #region constructor public Forwarding(JsonElement jsonForwarding, Dictionary configForwarders) { JsonElement jsonForwarders = jsonForwarding.GetProperty("forwarders"); List forwarderRecords = new List(); foreach (JsonElement jsonForwarder in jsonForwarders.EnumerateArray()) { string forwarderName = jsonForwarder.GetString(); if ((configForwarders is null) || !configForwarders.TryGetValue(forwarderName, out ConfigForwarder configForwarder)) throw new FormatException("Forwarder was not defined: " + forwarderName); forwarderRecords.AddRange(configForwarder.ForwarderRecords); } _forwarderRecords = forwarderRecords; _domainMap = jsonForwarding.ReadArrayAsMap("domains", delegate (JsonElement jsonDomain) { return new Tuple(jsonDomain.GetString().ToLowerInvariant(), null); }); } public Forwarding(IReadOnlyList domains, NameServerAddress forwarder, bool dnssecValidation, ConfigProxyServer proxy) : this(new DnsForwarderRecordData[] { GetForwarderRecord(forwarder, dnssecValidation, proxy) }, domains) { } public Forwarding(IReadOnlyList forwarderRecords, IReadOnlyList domains) { _forwarderRecords = forwarderRecords; Dictionary domainMap = new Dictionary(domains.Count); foreach (string domain in domains) { if (DnsClient.IsDomainNameValid(domain)) domainMap.TryAdd(domain.ToLowerInvariant(), null); } _domainMap = domainMap; } #endregion #region static public static bool TryGetForwarderRecords(string domain, IReadOnlyList forwardings, out IReadOnlyList forwarderRecords) { if (forwardings.Count == 1) { if (forwardings[0].TryGetForwarderRecords(domain, out forwarderRecords, out _)) return true; } else { Dictionary> fwdMap = new Dictionary>(forwardings.Count); foreach (Forwarding forwarding in forwardings) { if (forwarding.TryGetForwarderRecords(domain, out IReadOnlyList fwdRecords, out string matchedDomain)) { if (fwdMap.TryGetValue(matchedDomain, out List fwdRecordsList)) { fwdRecordsList.AddRange(fwdRecords); } else { fwdRecordsList = new List(fwdRecords); fwdMap.Add(matchedDomain, fwdRecordsList); } } } if (fwdMap.Count > 0) { forwarderRecords = null; string lastMatchedDomain = null; foreach (KeyValuePair> fwdEntry in fwdMap) { if ((lastMatchedDomain is null) || (fwdEntry.Key.Length > lastMatchedDomain.Length) || ((fwdEntry.Key.Length == lastMatchedDomain.Length) && lastMatchedDomain.StartsWith("*."))) { lastMatchedDomain = fwdEntry.Key; forwarderRecords = fwdEntry.Value; } } return true; } } forwarderRecords = null; return false; } public static bool IsForwarderDomain(string domain, IReadOnlyList forwardings) { foreach (Forwarding forwarding in forwardings) { if (IsForwarderDomain(domain, forwarding._forwarderRecords)) return true; } return false; } public static bool IsForwarderDomain(string domain, IReadOnlyList forwarderRecords) { foreach (DnsForwarderRecordData forwarderRecord in forwarderRecords) { if (domain.Equals(forwarderRecord.NameServer.Host, StringComparison.OrdinalIgnoreCase)) return true; } return false; } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private bool IsDomainMatching(string domain, out string matchedDomain) { string parent; do { if (_domainMap.TryGetValue(domain, out _)) { matchedDomain = domain; return true; } parent = GetParentZone(domain); if (parent is null) { if (_domainMap.TryGetValue("*", out _)) { matchedDomain = "*"; return true; } break; } domain = "*." + parent; if (_domainMap.TryGetValue(domain, out _)) { matchedDomain = domain; return true; } domain = parent; } while (true); matchedDomain = null; return false; } private bool TryGetForwarderRecords(string domain, out IReadOnlyList forwarderRecords, out string matchedDomain) { if (IsDomainMatching(domain, out matchedDomain)) { forwarderRecords = _forwarderRecords; return true; } forwarderRecords = null; return false; } #endregion #region public public void UpdateForwarderRecords(bool dnssecValidation, ConfigProxyServer proxy) { _forwarderRecords = GetUpdatedForwarderRecords(_forwarderRecords, dnssecValidation, proxy); } #endregion } class AdGuardUpstream : IDisposable { #region variables static readonly char[] _popWordSeperator = new char[] { ' ' }; readonly IDnsServer _dnsServer; readonly string _name; ConfigProxyServer _configProxyServer; bool _dnssecValidation; List _defaultForwarderRecords; List _forwardings; readonly string _configFile; DateTime _configFileLastModified; Timer _autoReloadTimer; const int AUTO_RELOAD_TIMER_INTERVAL = 60000; #endregion #region constructor public AdGuardUpstream(IDnsServer dnsServer, Dictionary configProxyServers, JsonElement jsonAdguardUpstream) { _dnsServer = dnsServer; _name = jsonAdguardUpstream.GetProperty("configFile").GetString(); _configFile = _name; if (!Path.IsPathRooted(_configFile)) _configFile = Path.Combine(_dnsServer.ApplicationFolder, _configFile); _autoReloadTimer = new Timer(delegate (object state) { try { DateTime configFileLastModified = File.GetLastWriteTimeUtc(_configFile); if (configFileLastModified > _configFileLastModified) { ReloadUpstreamsFile(); //force GC collection to remove old cache data from memory quickly GC.Collect(); } } catch (Exception ex) { _dnsServer.WriteLog(ex); } finally { _autoReloadTimer?.Change(AUTO_RELOAD_TIMER_INTERVAL, Timeout.Infinite); } }); ReloadConfig(configProxyServers, jsonAdguardUpstream); } #endregion #region IDisposable public void Dispose() { if (_autoReloadTimer is not null) { _autoReloadTimer.Dispose(); _autoReloadTimer = null; } } #endregion #region private private void ReloadUpstreamsFile() { try { _dnsServer.WriteLog("The app is reading AdGuard Upstreams config file: " + _configFile); List defaultForwarderRecords = new List(); List forwardings = new List(); using (FileStream fS = new FileStream(_configFile, FileMode.Open, FileAccess.Read)) { StreamReader sR = new StreamReader(fS, true); string line; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('#')) continue; //skip comment line if (line.StartsWith('[')) { int i = line.LastIndexOf(']'); if (i < 0) throw new FormatException("Invalid AdGuard Upstreams config file format: missing ']' bracket."); string[] domains = line.Substring(1, i - 1).Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); string forwarder = line.Substring(i + 1); if (forwarder == "#") { if (defaultForwarderRecords.Count == 0) throw new FormatException("Invalid AdGuard Upstreams config file format: missing default upstream servers."); forwardings.Add(new Forwarding(defaultForwarderRecords, domains)); } else { List forwarderRecords = new List(); string word = PopWord(ref forwarder); while (word.Length > 0) { string nextWord = PopWord(ref forwarder); if (nextWord.StartsWith('(')) { word += " " + nextWord; nextWord = PopWord(ref forwarder); } forwarderRecords.Add(GetForwarderRecord(NameServerAddress.Parse(word), _dnssecValidation, _configProxyServer)); word = nextWord; } if (forwarderRecords.Count == 0) throw new FormatException("Invalid AdGuard Upstreams config file format: missing upstream servers."); forwardings.Add(new Forwarding(forwarderRecords, domains)); } } else { defaultForwarderRecords.Add(GetForwarderRecord(NameServerAddress.Parse(line), _dnssecValidation, _configProxyServer)); } } _configFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle); } _defaultForwarderRecords = defaultForwarderRecords; _forwardings = forwardings; _dnsServer.WriteLog("The app has successfully loaded AdGuard Upstreams config file: " + _configFile); } catch (Exception ex) { _dnsServer.WriteLog("The app failed to read AdGuard Upstreams config file: " + _configFile + "\r\n" + ex.ToString()); } } private static string PopWord(ref string line) { if (line.Length == 0) return line; line = line.TrimStart(_popWordSeperator); int i = line.IndexOfAny(_popWordSeperator); string word; if (i < 0) { word = line; line = ""; } else { word = line.Substring(0, i); line = line.Substring(i + 1); } return word; } #endregion #region public public void ReloadConfig(Dictionary configProxyServers, JsonElement jsonAdguardUpstream) { string proxyName = jsonAdguardUpstream.GetPropertyValue("proxy", null); _dnssecValidation = jsonAdguardUpstream.GetPropertyValue("dnssecValidation", true); ConfigProxyServer configProxyServer = null; if (!string.IsNullOrEmpty(proxyName) && ((configProxyServers is null) || !configProxyServers.TryGetValue(proxyName, out configProxyServer))) throw new FormatException("Proxy server was not defined: " + proxyName); _configProxyServer = configProxyServer; DateTime configFileLastModified = File.GetLastWriteTimeUtc(_configFile); if (configFileLastModified > _configFileLastModified) { //reload complete config file _autoReloadTimer.Change(0, Timeout.Infinite); } else { //update only forwarder records _defaultForwarderRecords = GetUpdatedForwarderRecords(_defaultForwarderRecords, _dnssecValidation, _configProxyServer); foreach (Forwarding forwarding in _forwardings) forwarding.UpdateForwarderRecords(_dnssecValidation, _configProxyServer); } } public bool TryGetForwarderRecords(string domain, out IReadOnlyList forwarderRecords) { if ((_forwardings is not null) && (_forwardings.Count > 0)) { if (Forwarding.IsForwarderDomain(domain, _forwardings)) { forwarderRecords = null; return false; } if (Forwarding.TryGetForwarderRecords(domain, _forwardings, out forwarderRecords)) return true; } if ((_defaultForwarderRecords is not null) && (_defaultForwarderRecords.Count > 0)) { if (Forwarding.IsForwarderDomain(domain, _defaultForwarderRecords)) { forwarderRecords = null; return false; } forwarderRecords = _defaultForwarderRecords; return true; } forwarderRecords = null; return false; } #endregion #region property public string Name { get { return _name; } } #endregion } class ConfigProxyServer { #region variables readonly string _name; readonly DnsForwarderRecordProxyType _type; readonly string _proxyAddress; readonly ushort _proxyPort; readonly string _proxyUsername; readonly string _proxyPassword; #endregion #region constructor public ConfigProxyServer(JsonElement jsonProxy) { _name = jsonProxy.GetProperty("name").GetString(); _type = jsonProxy.GetPropertyEnumValue("type", DnsForwarderRecordProxyType.Http); _proxyAddress = jsonProxy.GetProperty("proxyAddress").GetString(); _proxyPort = jsonProxy.GetProperty("proxyPort").GetUInt16(); _proxyUsername = jsonProxy.GetPropertyValue("proxyUsername", null); _proxyPassword = jsonProxy.GetPropertyValue("proxyPassword", null); } #endregion #region properties public string Name { get { return _name; } } public DnsForwarderRecordProxyType Type { get { return _type; } } public string ProxyAddress { get { return _proxyAddress; } } public ushort ProxyPort { get { return _proxyPort; } } public string ProxyUsername { get { return _proxyUsername; } } public string ProxyPassword { get { return _proxyPassword; } } #endregion } class ConfigForwarder { #region variables readonly string _name; readonly DnsForwarderRecordData[] _forwarderRecords; #endregion #region constructor public ConfigForwarder(JsonElement jsonForwarder, Dictionary configProxyServers) { _name = jsonForwarder.GetProperty("name").GetString(); string proxyName = jsonForwarder.GetPropertyValue("proxy", null); bool dnssecValidation = jsonForwarder.GetPropertyValue("dnssecValidation", true); DnsTransportProtocol forwarderProtocol = jsonForwarder.GetPropertyEnumValue("forwarderProtocol", DnsTransportProtocol.Udp); ConfigProxyServer configProxyServer = null; if (!string.IsNullOrEmpty(proxyName) && ((configProxyServers is null) || !configProxyServers.TryGetValue(proxyName, out configProxyServer))) throw new FormatException("Proxy server was not defined: " + proxyName); _forwarderRecords = jsonForwarder.ReadArray("forwarderAddresses", delegate (string address) { return GetForwarderRecord(forwarderProtocol, address, dnssecValidation, configProxyServer); }); } #endregion #region properties public string Name { get { return _name; } } public DnsForwarderRecordData[] ForwarderRecords { get { return _forwarderRecords; } } #endregion } } } ================================================ FILE: Apps/AdvancedForwardingApp/adguard-upstreams.txt ================================================ # AdGuard Upstreams # File Format Reference: https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams # # Example: # 8.8.8.8 # udp://9.9.9.9 # [/host.com/example.com/]https://cloudflare-dns.com/dns-query (1.1.1.1) tls://1.1.1.1 # [/maps.host.com/]# # [/home/]192.168.10.2 # [/test.com/]https://dns.quad9.net/dns-query (9.9.9.9) ================================================ FILE: Apps/AdvancedForwardingApp/dnsApp.config ================================================ { "appPreference": 200, "enableForwarding": true, "proxyServers": [ { "name": "local-proxy", "type": "socks5", "proxyAddress": "localhost", "proxyPort": 1080, "proxyUsername": null, "proxyPassword": null } ], "forwarders": [ { "name": "quad9-doh", "proxy": null, "dnssecValidation": true, "forwarderProtocol": "Https", "forwarderAddresses": [ "https://dns.quad9.net/dns-query (9.9.9.9)" ] }, { "name": "cloudflare-google", "proxy": null, "dnssecValidation": true, "forwarderProtocol": "Tls", "forwarderAddresses": [ "1.1.1.1", "8.8.8.8" ] }, { "name": "quad9-tls-proxied", "proxy": "local-proxy", "dnssecValidation": true, "forwarderProtocol": "Tls", "forwarderAddresses": [ "9.9.9.9" ] } ], "networkGroupMap": { "0.0.0.0/0": "everyone", "[::]/0": "everyone" }, "groups": [ { "name": "everyone", "enableForwarding": true, "forwardings": [ { "forwarders": [ "quad9-doh" ], "domains": [ "example.com" ] }, { "forwarders": [ "cloudflare-google" ], "domains": [ "*" ] } ], "adguardUpstreams": [ { "proxy": null, "dnssecValidation": true, "configFile": "adguard-upstreams.txt" } ] } ] } ================================================ FILE: Apps/AutoPtrApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace AutoPtr { public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; return Task.CompletedTask; } public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; string qname = question.Name; if (qname.Length == appRecordName.Length) return null; if (!IPAddressExtensions.TryParseReverseDomain(qname.ToLowerInvariant(), out IPAddress address)) return null; if (question.Type != DnsResourceRecordType.PTR) { //NODATA reponse DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer); } string domain = null; using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; string ipSeparator; if (jsonAppRecordData.TryGetProperty("ipSeparator", out JsonElement jsonSeparator) && (jsonSeparator.ValueKind != JsonValueKind.Null)) ipSeparator = jsonSeparator.ToString(); else ipSeparator = string.Empty; switch (address.AddressFamily) { case AddressFamily.InterNetwork: { byte[] buffer = address.GetAddressBytes(); foreach (byte b in buffer) { if (domain is null) domain = b.ToString(); else domain += ipSeparator + b.ToString(); } } break; case AddressFamily.InterNetworkV6: { byte[] buffer = address.GetAddressBytes(); for (int i = 0; i < buffer.Length; i += 2) { if (domain is null) domain = buffer[i].ToString("x2") + buffer[i + 1].ToString("x2"); else domain += ipSeparator + buffer[i].ToString("x2") + buffer[i + 1].ToString("x2"); } } break; default: return null; } if (jsonAppRecordData.TryGetProperty("prefix", out JsonElement jsonPrefix) && (jsonPrefix.ValueKind != JsonValueKind.Null)) domain = jsonPrefix.GetString() + domain; if (jsonAppRecordData.TryGetProperty("suffix", out JsonElement jsonSuffix) && (jsonSuffix.ValueKind != JsonValueKind.Null)) domain += jsonSuffix.GetString(); } DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(qname, DnsResourceRecordType.PTR, DnsClass.IN, appRecordTtl, new DnsPTRRecordData(domain)) }; return new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer); } #endregion #region properties public string Description { get { return "Returns automatically generated response for a PTR request for both IPv4 and IPv6."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""prefix"": """", ""suffix"": "".example.com"", ""ipSeparator"": ""-"" }"; } } #endregion } } ================================================ FILE: Apps/AutoPtrApp/AutoPtrApp.csproj ================================================  net9.0 false true 4.0 false Technitium Technitium DNS Server Shreyas Zare AutoPtrApp AutoPtr https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return automatically generated response for a PTR request for both IPv4 and IPv6. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/AutoPtrApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/BlockPageApp/App.cs ================================================ /* 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.ApplicationCommon; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Security; 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.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace BlockPage { public sealed class App : IDnsApplication { #region variables IReadOnlyDictionary _webServers; #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; StopAllWebServersAsync().Sync(); _disposed = true; } #endregion #region private private async Task StopAllWebServersAsync() { if (_webServers is not null) { foreach (KeyValuePair webServerEntry in _webServers) await webServerEntry.Value.DisposeAsync(); _webServers = null; } } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; await StopAllWebServersAsync(); Dictionary webServers = new Dictionary(3); _webServers = webServers; if (jsonConfig.ValueKind == JsonValueKind.Array) { foreach (JsonElement jsonWebServerConfig in jsonConfig.EnumerateArray()) { string name = jsonWebServerConfig.GetPropertyValue("name", "default"); if (!webServers.TryGetValue(name, out WebServer webServer)) { webServer = new WebServer(dnsServer, name); if (!webServers.TryAdd(webServer.Name, webServer)) throw new InvalidOperationException("Failed to update web server config. Please try again."); } await webServer.InitializeAsync(jsonWebServerConfig); } } else { WebServer webServer = new WebServer(dnsServer, "default"); webServers.Add(webServer.Name, webServer); await webServer.InitializeAsync(jsonConfig); if (!jsonConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out _)) config = config.Replace("\"webServerTlsCertificateFilePath\"", "\"webServerUseSelfSignedTlsCertificate\": true,\r\n \"webServerTlsCertificateFilePath\""); if (!jsonConfig.TryGetProperty("enableWebServer", out _)) config = config.Replace("\"webServerLocalAddresses\"", "\"enableWebServer\": true,\r\n \"webServerLocalAddresses\""); if (!jsonConfig.TryGetProperty("name", out _)) config = config.Replace("\"enableWebServer\"", "\"name\": \"default\",\r\n \"enableWebServer\""); config = "[\r\n " + config.Replace("\n", "\n ").TrimEnd() + "\r\n]"; await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } } #endregion #region properties public string Description { get { return "Serves a block page from a built-in web server that can be displayed to the end user when a website is blocked by the DNS server.\n\nNote: You need to manually set the Blocking Type as Custom Address in the blocking settings and configure the current server's IP address as Custom Blocking Addresses for the block page to be served to the users. Use a PKCS #12 certificate (.pfx or .p12) for enabling HTTPS support. Enabling HTTPS support will show certificate error to the user which is expected and the user will have to proceed ignoring the certificate error to be able to see the block page."; } } #endregion class WebServer : IAsyncDisposable { #region variables readonly IDnsServer _dnsServer; readonly string _name; IReadOnlyList _webServerLocalAddresses = Array.Empty(); bool _webServerUseSelfSignedTlsCertificate; string _webServerTlsCertificateFilePath; string _webServerTlsCertificatePassword; string _webServerRootPath; bool _serveBlockPageFromWebServerRoot; bool _includeBlockingInfo; string _blockPageContent; WebApplication _webServer; SslServerAuthenticationOptions _sslServerAuthenticationOptions; DateTime _webServerTlsCertificateLastModifiedOn; Timer _tlsCertificateUpdateTimer; const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000; const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000; #endregion #region constructor public WebServer(IDnsServer dnsServer, string name) { _dnsServer = dnsServer; _name = name; } #endregion #region IDisposable bool _disposed; public async ValueTask DisposeAsync() { if (_disposed) return; await StopTlsCertificateUpdateTimerAsync(); await StopWebServerAsync(); _disposed = true; } #endregion #region private private async Task StartWebServerAsync() { WebApplicationBuilder builder = WebApplication.CreateBuilder(); if (_serveBlockPageFromWebServerRoot) { builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(_dnsServer.ApplicationFolder) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_webServerRootPath) { 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 _webServerLocalAddresses) serverOptions.Listen(webServiceLocalAddress, 80); //https if (_sslServerAuthenticationOptions is not null) { foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses) { serverOptions.Listen(webServiceLocalAddress, 443, delegate (ListenOptions listenOptions) { listenOptions.Protocols = HttpProtocols.Http1AndHttp2; listenOptions.UseHttps(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { return ValueTask.FromResult(_sslServerAuthenticationOptions); }, null); }); } } serverOptions.AddServerHeader = false; serverOptions.Limits.MaxRequestBodySize = int.MaxValue; }); builder.Logging.ClearProviders(); _webServer = builder.Build(); _webServer.UseResponseCompression(); _webServer.UseDefaultFiles(); _webServer.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 }); if (_serveBlockPageFromWebServerRoot) _webServer.Use(RedirectToDefaultPageAsync); else _webServer.Use(ServeDefaultPageAsync); try { await _webServer.StartAsync(); foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses) { _dnsServer.WriteLog("Web server '" + _name + "' was bound successfully: " + new IPEndPoint(webServiceLocalAddress, 80).ToString()); if (_sslServerAuthenticationOptions is not null) _dnsServer.WriteLog("Web server '" + _name + "' was bound successfully: " + new IPEndPoint(webServiceLocalAddress, 443).ToString()); } } catch (Exception ex) { await StopWebServerAsync(); foreach (IPAddress webServiceLocalAddress in _webServerLocalAddresses) { _dnsServer.WriteLog("Web server '" + _name + "' failed to bind: " + new IPEndPoint(webServiceLocalAddress, 80).ToString()); if (_sslServerAuthenticationOptions is not null) _dnsServer.WriteLog("Web server '" + _name + "' failed to bind: " + new IPEndPoint(webServiceLocalAddress, 443).ToString()); } _dnsServer.WriteLog(ex); } } private async Task StopWebServerAsync() { if (_webServer is not null) { await _webServer.DisposeAsync(); _webServer = null; } } private void LoadWebServiceTlsCertificate(string webServerTlsCertificateFilePath, string webServerTlsCertificatePassword) { FileInfo fileInfo = new FileInfo(webServerTlsCertificateFilePath); if (!fileInfo.Exists) throw new ArgumentException("Web server '" + _name + "' TLS certificate file does not exists: " + webServerTlsCertificateFilePath); switch (Path.GetExtension(webServerTlsCertificateFilePath).ToLowerInvariant()) { case ".pfx": case ".p12": break; default: throw new ArgumentException("Web server '" + _name + "' TLS certificate file must be PKCS #12 formatted with .pfx or .p12 extension: " + webServerTlsCertificateFilePath); } X509Certificate2Collection webServerTlsCertificateCollection = X509CertificateLoader.LoadPkcs12CollectionFromFile(webServerTlsCertificateFilePath, webServerTlsCertificatePassword, X509KeyStorageFlags.PersistKeySet); X509Certificate2 serverCertificate = null; foreach (X509Certificate2 certificate in webServerTlsCertificateCollection) { if (certificate.HasPrivateKey) { serverCertificate = certificate; break; } } if (serverCertificate is null) throw new ArgumentException("Web server '" + _name + "' TLS certificate file must contain a certificate with private key."); _sslServerAuthenticationOptions = new SslServerAuthenticationOptions() { ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, webServerTlsCertificateCollection, false) }; _webServerTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc; _dnsServer.WriteLog("Web server '" + _name + "' TLS certificate was loaded: " + webServerTlsCertificateFilePath); } private void StartTlsCertificateUpdateTimer() { if (_tlsCertificateUpdateTimer is null) { _tlsCertificateUpdateTimer = new Timer(delegate (object state) { if (!string.IsNullOrEmpty(_webServerTlsCertificateFilePath)) { try { FileInfo fileInfo = new FileInfo(_webServerTlsCertificateFilePath); if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _webServerTlsCertificateLastModifiedOn)) LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword); } catch (Exception ex) { _dnsServer.WriteLog("Web server '" + _name + "' encountered an error while updating TLS Certificate: " + _webServerTlsCertificateFilePath + "\r\n" + ex.ToString()); } } }, null, TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL, TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL); } } private async Task StopTlsCertificateUpdateTimerAsync() { if (_tlsCertificateUpdateTimer is not null) { await _tlsCertificateUpdateTimer.DisposeAsync(); _tlsCertificateUpdateTimer = null; } } private Task RedirectToDefaultPageAsync(HttpContext context, RequestDelegate next) { context.Response.Redirect("/", false, true); return Task.CompletedTask; } private async Task ServeDefaultPageAsync(HttpContext context, RequestDelegate next) { string blockPageContent = _blockPageContent; if (_includeBlockingInfo) { string blockingInfoHtmlContent = null; try { string host = context.Request.Host.Host; if (host is not null) { DnsDatagram dnsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [new DnsQuestionRecord(host, DnsResourceRecordType.A, DnsClass.IN)], udpPayloadSize: DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE); DnsDatagram dnsResponse = await _dnsServer.DirectQueryAsync(dnsRequest, 500); List options = new List(); if (dnsResponse.EDNS is not null) { foreach (EDnsOption option in dnsResponse.EDNS.Options) { if (option.Code == EDnsOptionCode.EXTENDED_DNS_ERROR) { EDnsExtendedDnsErrorOptionData ede = option.Data as EDnsExtendedDnsErrorOptionData; options.Add(ede); } } } options.AddRange(dnsResponse.DnsClientExtendedErrors); foreach (EDnsExtendedDnsErrorOptionData option in options) { if (blockingInfoHtmlContent is null) blockingInfoHtmlContent = "

Detailed Info
" + option.InfoCode.ToString() + (option.ExtraText is null ? "" : ": " + option.ExtraText); else blockingInfoHtmlContent += "
" + option.InfoCode.ToString() + (option.ExtraText is null ? "" : ": " + option.ExtraText); } if (blockingInfoHtmlContent is not null) blockingInfoHtmlContent += "

"; } } catch (Exception ex) { _dnsServer.WriteLog(ex); } if (blockingInfoHtmlContent is null) blockPageContent = blockPageContent.Replace("{BLOCKING-INFO}", ""); else blockPageContent = blockPageContent.Replace("{BLOCKING-INFO}", blockingInfoHtmlContent); } byte[] finalBlockPageContent = Encoding.UTF8.GetBytes(blockPageContent); HttpResponse response = context.Response; response.StatusCode = StatusCodes.Status200OK; response.ContentType = "text/html; charset=utf-8"; response.ContentLength = finalBlockPageContent.Length; using (Stream s = context.Response.Body) { await s.WriteAsync(finalBlockPageContent); } } #endregion #region public public async Task InitializeAsync(JsonElement jsonWebServerConfig) { bool enableWebServer = jsonWebServerConfig.GetPropertyValue("enableWebServer", true); if (!enableWebServer) { await StopWebServerAsync(); return; } _webServerLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(jsonWebServerConfig.ReadArray("webServerLocalAddresses", IPAddress.Parse)); if (jsonWebServerConfig.TryGetProperty("webServerUseSelfSignedTlsCertificate", out JsonElement jsonWebServerUseSelfSignedTlsCertificate)) _webServerUseSelfSignedTlsCertificate = jsonWebServerUseSelfSignedTlsCertificate.GetBoolean(); else _webServerUseSelfSignedTlsCertificate = true; _webServerTlsCertificateFilePath = jsonWebServerConfig.GetProperty("webServerTlsCertificateFilePath").GetString(); _webServerTlsCertificatePassword = jsonWebServerConfig.GetProperty("webServerTlsCertificatePassword").GetString(); _webServerRootPath = jsonWebServerConfig.GetProperty("webServerRootPath").GetString(); if (!Path.IsPathRooted(_webServerRootPath)) _webServerRootPath = Path.Combine(_dnsServer.ApplicationFolder, _webServerRootPath); _serveBlockPageFromWebServerRoot = jsonWebServerConfig.GetProperty("serveBlockPageFromWebServerRoot").GetBoolean(); string blockPageTitle = jsonWebServerConfig.GetProperty("blockPageTitle").GetString(); string blockPageHeading = jsonWebServerConfig.GetProperty("blockPageHeading").GetString(); string blockPageMessage = jsonWebServerConfig.GetProperty("blockPageMessage").GetString(); _includeBlockingInfo = jsonWebServerConfig.GetPropertyValue("includeBlockingInfo", true); _blockPageContent = @" " + (blockPageTitle is null ? "" : blockPageTitle) + @" " + (blockPageHeading is null ? "" : "

" + blockPageHeading + "

") + @" " + (blockPageMessage is null ? "" : "

" + blockPageMessage + "

") + @" " + (_includeBlockingInfo ? "{BLOCKING-INFO}" : "") + @" "; try { await StopWebServerAsync(); string selfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, "self-signed-cert.pfx"); if (_webServerUseSelfSignedTlsCertificate) { string oldSelfSignedCertificateFilePath = Path.Combine(_dnsServer.ApplicationFolder, "cert.pfx"); if (!oldSelfSignedCertificateFilePath.Equals(_webServerTlsCertificateFilePath, Environment.OSVersion.Platform == PlatformID.Win32NT ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) && File.Exists(oldSelfSignedCertificateFilePath) && !File.Exists(selfSignedCertificateFilePath)) File.Move(oldSelfSignedCertificateFilePath, selfSignedCertificateFilePath); if (!File.Exists(selfSignedCertificateFilePath)) { RSA rsa = RSA.Create(2048); CertificateRequest req = new CertificateRequest("cn=" + _dnsServer.ServerDomain, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(5)); await File.WriteAllBytesAsync(selfSignedCertificateFilePath, cert.Export(X509ContentType.Pkcs12, null as string)); } } else { File.Delete(selfSignedCertificateFilePath); } if (string.IsNullOrEmpty(_webServerTlsCertificateFilePath)) { await StopTlsCertificateUpdateTimerAsync(); if (_webServerUseSelfSignedTlsCertificate) { LoadWebServiceTlsCertificate(selfSignedCertificateFilePath, null); } else { //disable HTTPS _sslServerAuthenticationOptions = null; } } else { LoadWebServiceTlsCertificate(_webServerTlsCertificateFilePath, _webServerTlsCertificatePassword); StartTlsCertificateUpdateTimer(); } await StartWebServerAsync(); } catch (Exception ex) { _dnsServer.WriteLog(ex); } } #endregion #region properties public string Name { get { return _name; } } #endregion } } } ================================================ FILE: Apps/BlockPageApp/BlockPageApp.csproj ================================================  net9.0 false 7.1 false Technitium Technitium DNS Server Shreyas Zare BlockPageApp BlockPage https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Serves a block page from a built-in web server that can be displayed to the end user when a website is blocked by the DNS server.\n\nNote! You need to manually set the Blocking Type as Custom Address in the blocking settings and configure the current server's IP address as Custom Blocking Addresses for the block page to be served to the users. Use a PKCS #12 certificate (.pfx or .p12) for enabling HTTPS support. Enabling HTTPS support will show certificate error to the user which is expected and the user will have to proceed ignoring the certificate error to be able to see the block page. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest PreserveNewest ================================================ FILE: Apps/BlockPageApp/dnsApp.config ================================================ [ { "name": "default", "enableWebServer": true, "webServerLocalAddresses": [ "0.0.0.0", "::" ], "webServerUseSelfSignedTlsCertificate": true, "webServerTlsCertificateFilePath": null, "webServerTlsCertificatePassword": null, "webServerRootPath": "wwwroot", "serveBlockPageFromWebServerRoot": false, "blockPageTitle": "Website Blocked", "blockPageHeading": "Website Blocked", "blockPageMessage": "This website has been blocked by your network administrator.", "includeBlockingInfo": true } ] ================================================ FILE: Apps/BlockPageApp/wwwroot/index.html ================================================ Website Blocked

Website Blocked

This website has been blocked by your network administrator.

================================================ FILE: Apps/DefaultRecordsApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DefaultRecords { public sealed class App : IDnsApplication, IDnsPostProcessor { #region variables IDnsServer _dnsServer; bool _enableDefaultRecords; uint _defaultTtl; Dictionary _zoneSetMap; Dictionary _sets; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private bool TryGetMappedSets(string domain, out string zone, out string[] setNames) { domain = domain.ToLowerInvariant(); string parent; do { if (_zoneSetMap.TryGetValue(domain, out setNames)) { zone = domain; return true; } parent = GetParentZone(domain); if (parent is null) { if (_zoneSetMap.TryGetValue("*", out setNames)) { zone = "*"; return true; } break; } domain = "*." + parent; if (_zoneSetMap.TryGetValue(domain, out setNames)) { zone = domain; return true; } domain = parent; } while (true); zone = null; return false; } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _enableDefaultRecords = jsonConfig.GetProperty("enableDefaultRecords").GetBoolean(); _defaultTtl = jsonConfig.GetPropertyValue("defaultTtl", 3600u); _zoneSetMap = jsonConfig.ReadObjectAsMap("zoneSetMap", delegate (string zone, JsonElement jsonSets) { string[] sets = jsonSets.GetArray(); return new Tuple(zone.ToLowerInvariant(), sets); }); _sets = jsonConfig.ReadArrayAsMap("sets", delegate (JsonElement jsonSet) { Set set = new Set(jsonSet); return new Tuple(set.Name, set); }); return Task.CompletedTask; } public async Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (!_enableDefaultRecords) return response; if (!response.AuthoritativeAnswer || (response.OPCODE != DnsOpcode.StandardQuery)) return response; switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: break; default: return response; } DnsQuestionRecord question = request.Question[0]; if (!TryGetMappedSets(question.Name, out string zone, out string[] setNames)) return response; if (zone.StartsWith('*')) { DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.SOA, DnsClass.IN)); if (soaResponse is null) return response; if ((soaResponse.Answer.Count > 0) && (soaResponse.Answer[soaResponse.Answer.Count - 1].Type == DnsResourceRecordType.SOA)) zone = soaResponse.Answer[soaResponse.Answer.Count - 1].Name; else if ((soaResponse.Authority.Count > 0) && (soaResponse.Authority[0].Type == DnsResourceRecordType.SOA)) zone = soaResponse.Authority[0].Name; else return response; } StringBuilder sb = new StringBuilder(); foreach (string setName in setNames) { if (_sets.TryGetValue(setName, out Set set) && set.Enable) { foreach (string record in set.Records) sb.AppendLine(record); } } if (sb.Length == 0) return response; StringReader sR = new StringReader(sb.ToString()); List records = ZoneFile.ReadZoneFileFromAsync(sR, zone, _defaultTtl).Sync(); List newAnswer = new List(response.Answer.Count + records.Count); string qname = question.Name; if (response.Answer.Count > 0) { newAnswer.AddRange(response.Answer); DnsResourceRecord lastRR = response.Answer[response.Answer.Count - 1]; if (lastRR.Type == DnsResourceRecordType.CNAME) qname = (lastRR.RDATA as DnsCNAMERecordData).Domain; } foreach (DnsResourceRecord record in records) { if (record.Class != question.Class) continue; if ((record.Type != question.Type) && (record.Type != DnsResourceRecordType.CNAME)) continue; if (!record.Name.Equals(qname, StringComparison.OrdinalIgnoreCase)) continue; newAnswer.Add(record); if (record.Type == DnsResourceRecordType.CNAME) qname = (record.RDATA as DnsCNAMERecordData).Domain; } if (newAnswer.Count == response.Answer.Count) return response; return new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, newAnswer) { Tag = response.Tag }; } #endregion #region properties public string Description { get { return "Enables default records for configured local zones."; } } #endregion class Set { #region variables readonly string _name; readonly bool _enable; readonly string[] _records; #endregion #region constructor public Set(JsonElement jsonSet) { _name = jsonSet.GetProperty("name").GetString(); _enable = jsonSet.GetProperty("enable").GetBoolean(); _records = jsonSet.ReadArray("records"); } #endregion #region properties public string Name { get { return _name; } } public bool Enable { get { return _enable; } } public string[] Records { get { return _records; } } #endregion } } } ================================================ FILE: Apps/DefaultRecordsApp/DefaultRecordsApp.csproj ================================================  net9.0 false true 4.0 false Technitium Technitium DNS Server Shreyas Zare DefaultRecordsApp DefaultRecords https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows setting one or more default records for configured local zones. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/DefaultRecordsApp/dnsApp.config ================================================ { "enableDefaultRecords": false, "defaultTtl": 3600, "zoneSetMap": { "*": ["set1"], "*.net": ["set2"], "example.org": ["set1", "set2"] }, "sets": [ { "name": "set1", "enable": true, "records": [ "@ 3600 IN MX 10 mail.example.com.", "@ 3600 IN TXT \"v=spf1 a mx -all\"" ] }, { "name": "set2", "enable": true, "records": [ "www 3600 IN CNAME @", "@ 3600 IN A 1.2.3.4" ] } ] } ================================================ FILE: Apps/Dns64App/App.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Dns64 { // DNS64: DNS Extensions for Network Address Translation from IPv6 Clients to IPv4 Servers // https://www.rfc-editor.org/rfc/rfc6147 public sealed class App : IDnsApplication, IDnsPostProcessor, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference { #region variables IDnsServer _dnsServer; byte _appPreference; bool _enableDns64; Dictionary _networkGroupMap; Dictionary _groups; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue("appPreference", 30)); _enableDns64 = jsonConfig.GetProperty("enableDns64").GetBoolean(); _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string network, JsonElement group) { if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) throw new InvalidOperationException("Network group map contains an invalid network address: " + network); return new Tuple(networkAddress, group.GetString()); }); _groups = jsonConfig.ReadArrayAsMap("groups", delegate (JsonElement jsonGroup) { Group group = new Group(jsonGroup); return new Tuple(group.Name, group); }); return Task.CompletedTask; } public async Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (!_enableDns64) return response; if (request.DnssecOk) return response; switch (response.RCODE) { case DnsResponseCode.NxDomain: return response; } DnsQuestionRecord question = request.Question[0]; if (question.Type != DnsResourceRecordType.AAAA) return response; IPAddress remoteIP = remoteEP.Address; NetworkAddress network = null; string groupName = null; foreach (KeyValuePair entry in _networkGroupMap) { if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) { network = entry.Key; groupName = entry.Value; } } if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableDns64) return response; List newAnswer = new List(response.Answer.Count); bool synthesizeAAAA = true; if (group.ExcludedIpv6.Length == 0) { //no exclusions configured foreach (DnsResourceRecord answer in response.Answer) { newAnswer.Add(answer); if (answer.Type == DnsResourceRecordType.AAAA) synthesizeAAAA = false; //found an AAAA record so no need to synthesize AAAA } } else { //check for exclusions foreach (DnsResourceRecord answer in response.Answer) { if (answer.Type != DnsResourceRecordType.AAAA) { //keep non-AAAA record, most probably a CNAME record, in answer list newAnswer.Add(answer); continue; } IPAddress ipv6Address = (answer.RDATA as DnsAAAARecordData).Address; foreach (NetworkAddress excludedIpv6 in group.ExcludedIpv6) { if (!excludedIpv6.Contains(ipv6Address)) { //found non-excluded AAAA record so no need to synthesize AAAA newAnswer.Add(answer); synthesizeAAAA = false; } } } } if (!synthesizeAAAA) return new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, newAnswer, response.Authority, response.Additional) { Tag = response.Tag }; DnsDatagram newResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN), 2000); uint soaTtl; { DnsResourceRecord soa = response.FindFirstAuthorityRecord(); if ((soa is not null) && (soa.Type == DnsResourceRecordType.SOA)) soaTtl = soa.TTL; else soaTtl = 600; } foreach (DnsResourceRecord answer in newResponse.Answer) { if (answer.Type != DnsResourceRecordType.A) continue; IPAddress ipv4Address = (answer.RDATA as DnsARecordData).Address; NetworkAddress ipv4Network = null; NetworkAddress dns64Prefix = null; foreach (KeyValuePair dns64PrefixEntry in group.Dns64PrefixMap) { if (dns64PrefixEntry.Key.Contains(ipv4Address) && ((ipv4Network is null) || (dns64PrefixEntry.Key.PrefixLength > ipv4Network.PrefixLength))) { ipv4Network = dns64PrefixEntry.Key; dns64Prefix = dns64PrefixEntry.Value; } } if (dns64Prefix is null) continue; IPAddress ipv6Address = ipv4Address.MapToIPv6(dns64Prefix); newAnswer.Add(new DnsResourceRecord(answer.Name, DnsResourceRecordType.AAAA, answer.Class, Math.Min(answer.TTL, soaTtl), new DnsAAAARecordData(ipv6Address))); } return new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, newResponse.RCODE, response.Question, newAnswer, newResponse.Authority, newResponse.Additional) { Tag = response.Tag }; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { if (!_enableDns64) return Task.FromResult(null); if (request.DnssecOk) return Task.FromResult(null); DnsQuestionRecord question = request.Question[0]; if ((question.Type != DnsResourceRecordType.PTR) || !question.Name.EndsWith(".ip6.arpa", StringComparison.OrdinalIgnoreCase)) return Task.FromResult(null); IPAddress remoteIP = remoteEP.Address; NetworkAddress network = null; string groupName = null; foreach (KeyValuePair entry in _networkGroupMap) { if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) { network = entry.Key; groupName = entry.Value; } } if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.EnableDns64) return Task.FromResult(null); IPAddress ipv6Address = IPAddressExtensions.ParseReverseDomain(question.Name); if (ipv6Address.AddressFamily != AddressFamily.InterNetworkV6) return Task.FromResult(null); NetworkAddress dns64Prefix = null; foreach (KeyValuePair dns64PrefixEntry in group.Dns64PrefixMap) { if ((dns64PrefixEntry.Value is not null) && dns64PrefixEntry.Value.Contains(ipv6Address)) { dns64Prefix = dns64PrefixEntry.Value; break; } } if (dns64Prefix is null) return Task.FromResult(null); IPAddress ipv4Address = ipv6Address.MapToIPv4(dns64Prefix.PrefixLength); IReadOnlyList answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, 600, new DnsCNAMERecordData(ipv4Address.GetReverseDomain())) }; return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer)); } #endregion #region properties public string Description { get { return "Enables DNS64 function for both authoritative and recursive resolver responses for use by IPv6 only clients."; } } public byte Preference { get { return _appPreference; } } #endregion class Group { #region variables readonly string _name; readonly bool _enableDns64; readonly Dictionary _dns64PrefixMap; readonly NetworkAddress[] _excludedIpv6; #endregion #region constructor public Group(JsonElement jsonGroup) { _name = jsonGroup.GetProperty("name").GetString(); _enableDns64 = jsonGroup.GetProperty("enableDns64").GetBoolean(); _dns64PrefixMap = jsonGroup.ReadObjectAsMap("dns64PrefixMap", delegate (string strNetwork, JsonElement jsonDns64Prefix) { string strDns64Prefix = jsonDns64Prefix.GetString(); NetworkAddress network = NetworkAddress.Parse(strNetwork); NetworkAddress dns64Prefix = null; if (strDns64Prefix is not null) { dns64Prefix = NetworkAddress.Parse(strDns64Prefix); switch (dns64Prefix.PrefixLength) { case 32: case 40: case 48: case 56: case 64: case 96: break; default: throw new NotSupportedException("DNS64 prefix can have only the following prefixes: 32, 40, 48, 56, 64, or 96."); } } return new Tuple(network, dns64Prefix); }); _excludedIpv6 = jsonGroup.ReadArray("excludedIpv6", delegate (string strNetworkAddress) { NetworkAddress networkAddress = NetworkAddress.Parse(strNetworkAddress); if (networkAddress.Address.AddressFamily != AddressFamily.InterNetworkV6) throw new InvalidOperationException("An IPv6 network address is expected for 'excludedIpv6' array."); return networkAddress; }); } #endregion #region properties public string Name { get { return _name; } } public bool EnableDns64 { get { return _enableDns64; } } public Dictionary Dns64PrefixMap { get { return _dns64PrefixMap; } } public NetworkAddress[] ExcludedIpv6 { get { return _excludedIpv6; } } #endregion } } } ================================================ FILE: Apps/Dns64App/Dns64App.csproj ================================================  net9.0 false 5.0 false Technitium Technitium DNS Server Shreyas Zare Dns64App Dns64 https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Enables DNS64 function for both authoritative and recursive resolver responses for use by IPv6 only clients.\n\nWarning! Installing DNS64 app without having NAT64 in place will cause connectivity issues for some websites. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/Dns64App/dnsApp.config ================================================ { "appPreference": 30, "enableDns64": true, "networkGroupMap": { "::/0": "everyone" }, "groups": [ { "name": "everyone", "enableDns64": true, "dns64PrefixMap": { "0.0.0.0/0": "64:ff9b::/96", "10.0.0.0/8": null, "172.16.0.0/12": null, "192.168.0.0/16": null }, "excludedIpv6": [ "::ffff:0:0/96" ] } ] } ================================================ FILE: Apps/DnsBlockListApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsBlockList { //DNS Blacklists and Whitelists //https://www.rfc-editor.org/rfc/rfc5782 public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; Dictionary _dnsBlockLists; #endregion #region IDisposable public void Dispose() { if (_dnsBlockLists is not null) { foreach (KeyValuePair dnsBlockList in _dnsBlockLists) dnsBlockList.Value.Dispose(); _dnsBlockLists = null; } } #endregion #region private private static bool TryParseDnsblDomain(string qName, string appRecordName, out IPAddress address, out string domain) { qName = qName.Substring(0, qName.Length - appRecordName.Length - 1); string[] parts = qName.Split('.'); string lastPart = parts[parts.Length - 1]; if (byte.TryParse(lastPart, out _) || byte.TryParse(lastPart, NumberStyles.HexNumber, null, out _)) { switch (parts.Length) { case 4: { Span buffer = stackalloc byte[4]; for (int i = 0, j = parts.Length - 1; (i < 4) && (j > -1); i++, j--) buffer[i] = byte.Parse(parts[j]); address = new IPAddress(buffer); domain = null; return true; } case 32: { Span buffer = stackalloc byte[16]; for (int i = 0, j = parts.Length - 1; (i < 16) && (j > 0); i++, j -= 2) buffer[i] = (byte)(byte.Parse(parts[j], NumberStyles.HexNumber) << 4 | byte.Parse(parts[j - 1], NumberStyles.HexNumber)); address = new IPAddress(buffer); domain = null; return true; } default: address = null; domain = null; return false; } } else { address = null; domain = lastPart; for (int i = parts.Length - 2; i > -1; i--) domain = parts[i] + "." + domain; return true; } } private Tuple ReadBlockList(JsonElement jsonBlockList) { BlockList blockList; string name = jsonBlockList.GetProperty("name").GetString(); BlockListType type = jsonBlockList.GetPropertyEnumValue("type", BlockListType.Ip); if ((_dnsBlockLists is not null) && _dnsBlockLists.TryGetValue(name, out BlockList existingBlockList) && (existingBlockList.Type == type)) { existingBlockList.ReloadConfig(jsonBlockList); blockList = existingBlockList; } else { switch (type) { case BlockListType.Ip: blockList = new IpBlockList(_dnsServer, jsonBlockList); break; case BlockListType.Domain: blockList = new DomainBlockList(_dnsServer, jsonBlockList); break; default: throw new NotSupportedException("DNSBL block list type is not supported: " + type.ToString()); } } return new Tuple(blockList.Name, blockList); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; if (jsonConfig.TryReadArrayAsMap("dnsBlockLists", ReadBlockList, out Dictionary dnsBlockLists)) { if (_dnsBlockLists is not null) { foreach (KeyValuePair dnsBlockList in _dnsBlockLists) { if (!dnsBlockLists.ContainsKey(dnsBlockList.Key)) dnsBlockList.Value.Dispose(); } } _dnsBlockLists = dnsBlockLists; } else { if (_dnsBlockLists is not null) { foreach (KeyValuePair dnsBlockList in _dnsBlockLists) dnsBlockList.Value.Dispose(); } _dnsBlockLists = null; } return Task.CompletedTask; } public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; string qname = question.Name; if (qname.Length == appRecordName.Length) return null; if ((_dnsBlockLists is null) || !TryParseDnsblDomain(qname, appRecordName, out IPAddress address, out string domain)) return null; using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; if (jsonAppRecordData.TryReadArray("dnsBlockLists", out string[] dnsBlockLists)) { bool isBlocked = false; IPAddress responseA = null; string responseTXT = null; if (address is not null) { foreach (string dnsBlockList in dnsBlockLists) { if (_dnsBlockLists.TryGetValue(dnsBlockList, out BlockList blockList) && blockList.Enabled && (blockList.Type == BlockListType.Ip) && blockList.IsBlocked(address, out responseA, out responseTXT)) { isBlocked = true; if (!string.IsNullOrEmpty(responseTXT)) responseTXT = responseTXT.Replace("{ip}", address.ToString()); break; } } } else if (domain is not null) { foreach (string dnsBlockList in dnsBlockLists) { if (_dnsBlockLists.TryGetValue(dnsBlockList, out BlockList blockList) && blockList.Enabled && (blockList.Type == BlockListType.Domain) && blockList.IsBlocked(domain, out string foundDomain, out responseA, out responseTXT)) { isBlocked = true; if (!string.IsNullOrEmpty(responseTXT)) responseTXT = responseTXT.Replace("{domain}", foundDomain); break; } } } if (isBlocked) { switch (question.Type) { case DnsResourceRecordType.A: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { new DnsResourceRecord(qname, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecordData(responseA)) }); case DnsResourceRecordType.TXT: if (!string.IsNullOrEmpty(responseTXT)) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { new DnsResourceRecord(qname, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(responseTXT)) }); break; } //NODATA response DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer); } } return null; } #endregion #region properties public string Description { get { return "Returns A or TXT records based on the DNS Block Lists (DNSBL) configured in the APP record data. Returns NXDOMAIN response when an IP address or domain name is not blocked in any of the configured blocklists."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""dnsBlockLists"": [ ""ipblocklist1"", ""domainblocklist1"" ] }"; } } #endregion enum BlockListType { Ip = 1, Domain = 2 } abstract class BlockList : IDisposable { #region variables protected static readonly char[] _popWordSeperator = new char[] { ' ', '\t', '|' }; protected readonly IDnsServer _dnsServer; readonly BlockListType _type; readonly string _name; bool _enabled; protected IPAddress _responseA; protected string _responseTXT; protected string _blockListFile; protected DateTime _blockListFileLastModified; Timer _autoReloadTimer; const int AUTO_RELOAD_TIMER_INTERVAL = 60000; #endregion #region constructor protected BlockList(IDnsServer dnsServer, BlockListType type, JsonElement jsonBlockList) { _dnsServer = dnsServer; _type = type; _name = jsonBlockList.GetProperty("name").GetString(); _autoReloadTimer = new Timer(delegate (object state) { try { DateTime blockListFileLastModified = File.GetLastWriteTimeUtc(_blockListFile); if (blockListFileLastModified > _blockListFileLastModified) ReloadBlockListFile(); } catch (Exception ex) { _dnsServer.WriteLog(ex); } finally { _autoReloadTimer?.Change(AUTO_RELOAD_TIMER_INTERVAL, Timeout.Infinite); } }); ReloadConfig(jsonBlockList); } #endregion #region IDisposable public void Dispose() { if (_autoReloadTimer is not null) { _autoReloadTimer.Dispose(); _autoReloadTimer = null; } } #endregion #region protected protected abstract void ReloadBlockListFile(); protected static string PopWord(ref string line) { if (line.Length == 0) return line; line = line.TrimStart(_popWordSeperator); int i = line.IndexOfAny(_popWordSeperator); string word; if (i < 0) { word = line; line = ""; } else { word = line.Substring(0, i); line = line.Substring(i + 1); } return word; } #endregion #region public public void ReloadConfig(JsonElement jsonBlockList) { _enabled = jsonBlockList.GetPropertyValue("enabled", true); _responseA = IPAddress.Parse(jsonBlockList.GetPropertyValue("responseA", "127.0.0.2")); if (jsonBlockList.TryGetProperty("responseTXT", out JsonElement jsonResponseTXT)) _responseTXT = jsonResponseTXT.GetString(); else _responseTXT = null; string blockListFile = jsonBlockList.GetProperty("blockListFile").GetString(); if (!Path.IsPathRooted(blockListFile)) blockListFile = Path.Combine(_dnsServer.ApplicationFolder, blockListFile); if (!blockListFile.Equals(_blockListFile)) { _blockListFile = blockListFile; _blockListFileLastModified = default; } _autoReloadTimer.Change(0, Timeout.Infinite); } public virtual bool IsBlocked(IPAddress address, out IPAddress responseA, out string responseTXT) { throw new InvalidOperationException(); } public virtual bool IsBlocked(string domain, out string foundDomain, out IPAddress responseA, out string responseTXT) { throw new InvalidOperationException(); } #endregion #region properties public BlockListType Type { get { return _type; } } public string Name { get { return _name; } } public bool Enabled { get { return _enabled; } } public IPAddress ResponseA { get { return _responseA; } } public string ResponseTXT { get { return _responseTXT; } } public string BlockListFile { get { return _blockListFile; } } #endregion } class BlockEntry { #region variables readonly T _key; readonly IPAddress _responseA; readonly string _responseTXT; #endregion #region constructor public BlockEntry(T key, string responseA, string responseTXT) { _key = key; if (IPAddress.TryParse(responseA, out IPAddress addr)) _responseA = addr; if (!string.IsNullOrEmpty(responseTXT)) _responseTXT = responseTXT; } #endregion #region properties public T Key { get { return _key; } } public IPAddress ResponseA { get { return _responseA; } } public string ResponseTXT { get { return _responseTXT; } } #endregion } class IpBlockList : BlockList { #region variables Dictionary> _ipv4Map; Dictionary> _ipv6Map; NetworkMap> _ipv4NetworkMap; NetworkMap> _ipv6NetworkMap; #endregion #region constructor public IpBlockList(IDnsServer dnsServer, JsonElement jsonBlockList) : base(dnsServer, BlockListType.Ip, jsonBlockList) { } #endregion #region protected protected override void ReloadBlockListFile() { try { _dnsServer.WriteLog("The app is reading IP block list file: " + _blockListFile); //parse ip block list file Queue> ipv4Addresses = new Queue>(); Queue> ipv6Addresses = new Queue>(); Queue> ipv4Networks = new Queue>(); Queue> ipv6Networks = new Queue>(); ipv4Addresses.Enqueue(new BlockEntry(IPAddress.Parse("127.0.0.2"), "127.0.0.2", "rfc5782 test entry")); ipv6Addresses.Enqueue(new BlockEntry(IPAddress.Parse("::FFFF:7F00:2"), "127.0.0.2", "rfc5782 test entry")); using (FileStream fS = new FileStream(_blockListFile, FileMode.Open, FileAccess.Read)) { StreamReader sR = new StreamReader(fS, true); string line; string network; string responseA; string responseTXT; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(_popWordSeperator); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('#')) continue; //skip comment line network = PopWord(ref line); responseA = PopWord(ref line); responseTXT = line; if (NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) { switch (networkAddress.AddressFamily) { case AddressFamily.InterNetwork: if (networkAddress.PrefixLength == 32) ipv4Addresses.Enqueue(new BlockEntry(networkAddress.Address, responseA, responseTXT)); else ipv4Networks.Enqueue(new BlockEntry(networkAddress, responseA, responseTXT)); break; case AddressFamily.InterNetworkV6: if (networkAddress.PrefixLength == 128) ipv6Addresses.Enqueue(new BlockEntry(networkAddress.Address, responseA, responseTXT)); else ipv6Networks.Enqueue(new BlockEntry(networkAddress, responseA, responseTXT)); break; } } } _blockListFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle); } //load ip lookup list Dictionary> ipv4AddressMap = new Dictionary>(ipv4Addresses.Count); while (ipv4Addresses.Count > 0) { BlockEntry entry = ipv4Addresses.Dequeue(); ipv4AddressMap.TryAdd(entry.Key, entry); } Dictionary> ipv6AddressMap = new Dictionary>(ipv6Addresses.Count); while (ipv6Addresses.Count > 0) { BlockEntry entry = ipv6Addresses.Dequeue(); ipv6AddressMap.TryAdd(entry.Key, entry); } NetworkMap> ipv4NetworkMap = new NetworkMap>(ipv4Networks.Count); while (ipv4Networks.Count > 0) { BlockEntry entry = ipv4Networks.Dequeue(); ipv4NetworkMap.Add(entry.Key, entry); } NetworkMap> ipv6NetworkMap = new NetworkMap>(ipv6Networks.Count); while (ipv6Networks.Count > 0) { BlockEntry entry = ipv6Networks.Dequeue(); ipv6NetworkMap.Add(entry.Key, entry); } //update _ipv4Map = ipv4AddressMap; _ipv6Map = ipv6AddressMap; _ipv4NetworkMap = ipv4NetworkMap; _ipv6NetworkMap = ipv6NetworkMap; _dnsServer.WriteLog("The app has successfully loaded IP block list file: " + _blockListFile); } catch (Exception ex) { _dnsServer.WriteLog("The app failed to read IP block list file: " + _blockListFile + "\r\n" + ex.ToString()); } } #endregion #region public public override bool IsBlocked(IPAddress address, out IPAddress responseA, out string responseTXT) { switch (address.AddressFamily) { case AddressFamily.InterNetwork: { if (_ipv4Map.TryGetValue(address, out BlockEntry ipEntry)) { responseA = ipEntry.ResponseA is null ? _responseA : ipEntry.ResponseA; responseTXT = ipEntry.ResponseTXT is null ? _responseTXT : ipEntry.ResponseTXT; return true; } if (_ipv4NetworkMap.TryGetValue(address, out BlockEntry networkEntry)) { responseA = networkEntry.ResponseA is null ? _responseA : networkEntry.ResponseA; responseTXT = networkEntry.ResponseTXT is null ? _responseTXT : networkEntry.ResponseTXT; return true; } } break; case AddressFamily.InterNetworkV6: { if (_ipv6Map.TryGetValue(address, out BlockEntry ipEntry)) { responseA = ipEntry.ResponseA is null ? _responseA : ipEntry.ResponseA; responseTXT = ipEntry.ResponseTXT is null ? _responseTXT : ipEntry.ResponseTXT; return true; } if (_ipv6NetworkMap.TryGetValue(address, out BlockEntry networkEntry)) { responseA = networkEntry.ResponseA is null ? _responseA : networkEntry.ResponseA; responseTXT = networkEntry.ResponseTXT is null ? _responseTXT : networkEntry.ResponseTXT; return true; } } break; } responseA = null; responseTXT = null; return false; } #endregion } class DomainBlockList : BlockList { #region variables Dictionary> _domainMap; #endregion #region constructor public DomainBlockList(IDnsServer dnsServer, JsonElement jsonIpBlockList) : base(dnsServer, BlockListType.Domain, jsonIpBlockList) { } #endregion #region protected protected override void ReloadBlockListFile() { try { _dnsServer.WriteLog("The app is reading domain block list file: " + _blockListFile); //parse ip block list file Queue> domains = new Queue>(); domains.Enqueue(new BlockEntry("test", "127.0.0.2", "rfc5782 test entry")); using (FileStream fS = new FileStream(_blockListFile, FileMode.Open, FileAccess.Read)) { StreamReader sR = new StreamReader(fS, true); char[] trimSeperator = new char[] { ' ', '\t', ':', '|', ',' }; string line; string domain; string responseA; string responseTXT; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(trimSeperator); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('#')) continue; //skip comment line domain = PopWord(ref line); responseA = PopWord(ref line); responseTXT = line; if (DnsClient.IsDomainNameValid(domain)) domains.Enqueue(new BlockEntry(domain.ToLowerInvariant(), responseA, responseTXT)); } _blockListFileLastModified = File.GetLastWriteTimeUtc(fS.SafeFileHandle); } //load ip lookup list Dictionary> domainMap = new Dictionary>(domains.Count); while (domains.Count > 0) { BlockEntry entry = domains.Dequeue(); domainMap.TryAdd(entry.Key, entry); } //update _domainMap = domainMap; _dnsServer.WriteLog("The app has successfully loaded domain block list file: " + _blockListFile); } catch (Exception ex) { _dnsServer.WriteLog("The app failed to read domain block list file: " + _blockListFile + "\r\n" + ex.ToString()); } } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private bool IsDomainBlocked(string domain, out BlockEntry domainEntry) { do { if (_domainMap.TryGetValue(domain, out domainEntry)) { return true; } domain = GetParentZone(domain); } while (domain is not null); return false; } #endregion #region public public override bool IsBlocked(string domain, out string foundDomain, out IPAddress responseA, out string responseTXT) { if (IsDomainBlocked(domain.ToLowerInvariant(), out BlockEntry domainEntry)) { foundDomain = domainEntry.Key; responseA = domainEntry.ResponseA is null ? _responseA : domainEntry.ResponseA; responseTXT = domainEntry.ResponseTXT is null ? _responseTXT : domainEntry.ResponseTXT; return true; } foundDomain = null; responseA = null; responseTXT = null; return false; } #endregion } } } ================================================ FILE: Apps/DnsBlockListApp/DnsBlockListApp.csproj ================================================  net9.0 false 4.0 false Technitium Technitium DNS Server Shreyas Zare DnsBlockListApp DnsBlockList https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return A or TXT records based on the DNS Block Lists (DNSBL) configured. The implementation is based on RFC 5782. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: Apps/DnsBlockListApp/dnsApp.config ================================================ { "dnsBlockLists": [ { "name": "ipblocklist1", "type": "ip", "enabled": true, "responseA": "127.0.0.2", "responseTXT": "https://example.com/dnsbl?ip={ip}", "blockListFile": "ip-blocklist.txt" }, { "name": "domainblocklist1", "type": "domain", "enabled": true, "responseA": "127.0.0.2", "responseTXT": "https://example.com/dnsbl?domain={domain}", "blockListFile": "domain-blocklist.txt" } ] } ================================================ FILE: Apps/DnsBlockListApp/domain-blocklist.txt ================================================ # DNSBL domain block list # Format: domain A-response TXT-response # Seperator: , , or char # # A-response & TXT-response are optional but A-response must exists when TXT-response is specified # # Examples: # example.com # example.net 127.0.0.4 # malware.com 127.0.0.4 malware see: https://example.com/dnsbl?domain={domain} ================================================ FILE: Apps/DnsBlockListApp/ip-blocklist.txt ================================================ # DNSBL IP block list # Format: ip/network A-response TXT-response # Seperator: , , or char # # A-response & TXT-response are optional but A-response must exists when TXT-response is specified. # Supports both IPv4 and IPv6 addresses. # # Examples: # 192.168.1.1 # 192.168.0.0/24 # 192.168.2.1 127.0.0.3 # 10.8.1.0/24 127.0.0.3 malware see: https://example.com/dnsbl?ip={ip} # 2001:db8::/64 ================================================ FILE: Apps/DnsRebindingProtectionApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System.Collections.Generic; using System.IO; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsRebindingProtection { public sealed class App : IDnsApplication, IDnsPostProcessor { #region variables bool _enableProtection; NetworkAddress[] _bypassNetworks; HashSet _privateNetworks; HashSet _privateDomains; #endregion #region IDisposable public void Dispose() { // Nothing to dispose of. } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain[(i + 1)..]; //dont return root zone return null; } private bool IsPrivateDomain(string domain) { domain = domain.ToLowerInvariant(); do { if (_privateDomains.Contains(domain)) return true; domain = GetParentZone(domain); } while (domain is not null); return false; } private bool IsRebindingAttempt(DnsResourceRecord record) { IPAddress address; switch (record.Type) { case DnsResourceRecordType.A: if (IsPrivateDomain(record.Name)) return false; address = (record.RDATA as DnsARecordData).Address; break; case DnsResourceRecordType.AAAA: if (IsPrivateDomain(record.Name)) return false; address = (record.RDATA as DnsAAAARecordData).Address; break; default: return false; } foreach (NetworkAddress networkAddress in _privateNetworks) { if (networkAddress.Contains(address)) return true; } return false; } private bool TryDetectRebinding(IReadOnlyList answer, out List protectedAnswer) { for (int i = 0; i < answer.Count; i++) { DnsResourceRecord record = answer[i]; if (IsRebindingAttempt(record)) { //rebinding attempt detected! //prepare protected answer protectedAnswer = new List(answer.Count); //copy passed records for (int j = 0; j < i; j++) protectedAnswer.Add(answer[j]); //copy remaining records with check for (int j = i + 1; j < answer.Count; j++) { record = answer[j]; if (!IsRebindingAttempt(record)) protectedAnswer.Add(record); } return true; } } protectedAnswer = null; return false; } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _enableProtection = jsonConfig.GetPropertyValue("enableProtection", true); _privateNetworks = new HashSet(jsonConfig.ReadArray("privateNetworks", NetworkAddress.Parse)); _privateDomains = new HashSet(jsonConfig.ReadArray("privateDomains")); if (jsonConfig.TryReadArray("bypassNetworks", NetworkAddress.Parse, out NetworkAddress[] bypassNetworks)) { _bypassNetworks = bypassNetworks; } else { _bypassNetworks = []; //update config for new feature config = config.Replace("\"privateNetworks\"", "\"bypassNetworks\": [\r\n ],\r\n \"privateNetworks\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } } public Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { // Do not filter authoritative responses. Because in this case any rebinding is intentional. if (!_enableProtection || response.AuthoritativeAnswer) return Task.FromResult(response); IPAddress remoteIP = remoteEP.Address; foreach (NetworkAddress network in _bypassNetworks) { if (network.Contains(remoteIP)) return Task.FromResult(response); } if (TryDetectRebinding(response.Answer, out List protectedAnswer)) return Task.FromResult(response.Clone(protectedAnswer)); return Task.FromResult(response); } #endregion #region properties public string Description { get { return "Protects from DNS rebinding attacks using configured private domains and networks."; } } #endregion } } ================================================ FILE: Apps/DnsRebindingProtectionApp/DnsRebindingProtectionApp.csproj ================================================  net9.0 false 4.0 false Technitium Technitium DNS Server Shreyas Zare, Rui Fung Yip DnsRebindingProtectionApp DnsRebindingProtection https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Protects from DNS rebinding attacks using configured private domains and networks.\n\nWarning! The app will remove private IP addresses from response for domain names not hosted locally. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/DnsRebindingProtectionApp/dnsApp.config ================================================ { "enableProtection": true, "bypassNetworks": [ ], "privateNetworks": [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "fc00::/7", "fe80::/10" ], "privateDomains": [ "home.arpa" ] } ================================================ FILE: Apps/DropRequestsApp/App.cs ================================================ /* 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.ApplicationCommon; using System; using System.IO; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DropRequests { public sealed class App : IDnsApplication, IDnsRequestController { #region variables bool _enableBlocking; bool _dropMalformedRequests; NetworkAddress[] _allowedNetworks; NetworkAddress[] _blockedNetworks; BlockedQuestion[] _blockedQuestions; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _enableBlocking = jsonConfig.GetProperty("enableBlocking").GetBoolean(); if (jsonConfig.TryGetProperty("dropMalformedRequests", out JsonElement jsonDropMalformedRequests)) _dropMalformedRequests = jsonDropMalformedRequests.GetBoolean(); else _dropMalformedRequests = false; if (jsonConfig.TryReadArray("allowedNetworks", NetworkAddress.Parse, out NetworkAddress[] allowedNetworks)) _allowedNetworks = allowedNetworks; else _allowedNetworks = Array.Empty(); if (jsonConfig.TryReadArray("blockedNetworks", NetworkAddress.Parse, out NetworkAddress[] blockedNetworks)) _blockedNetworks = blockedNetworks; else _blockedNetworks = Array.Empty(); if (jsonConfig.TryReadArray("blockedQuestions", delegate (JsonElement blockedQuestion) { return new BlockedQuestion(blockedQuestion); }, out BlockedQuestion[] blockedQuestions)) _blockedQuestions = blockedQuestions; else _blockedQuestions = Array.Empty(); if (!jsonConfig.TryGetProperty("dropMalformedRequests", out _)) { config = config.Replace("\"allowedNetworks\"", "\"dropMalformedRequests\": false,\r\n \"allowedNetworks\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } } public Task GetRequestActionAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { if (!_enableBlocking) return Task.FromResult(DnsRequestControllerAction.Allow); if (_dropMalformedRequests && (request.ParsingException is not null)) return Task.FromResult(DnsRequestControllerAction.DropSilently); IPAddress remoteIp = remoteEP.Address; foreach (NetworkAddress allowedNetwork in _allowedNetworks) { if (allowedNetwork.Contains(remoteIp)) return Task.FromResult(DnsRequestControllerAction.Allow); } foreach (NetworkAddress blockedNetwork in _blockedNetworks) { if (blockedNetwork.Contains(remoteIp)) return Task.FromResult(DnsRequestControllerAction.DropSilently); } if (request.Question.Count != 1) return Task.FromResult(DnsRequestControllerAction.DropSilently); DnsQuestionRecord requestQuestion = request.Question[0]; foreach (BlockedQuestion blockedQuestion in _blockedQuestions) { if (blockedQuestion.Matches(requestQuestion)) return Task.FromResult(DnsRequestControllerAction.DropSilently); } return Task.FromResult(DnsRequestControllerAction.Allow); } #endregion #region properties public string Description { get { return "Drops incoming DNS requests that match list of blocked networks or blocked questions."; } } #endregion class BlockedQuestion { #region variables readonly string _name; readonly bool _blockZone; readonly DnsResourceRecordType _type; #endregion #region constructor public BlockedQuestion(JsonElement jsonQuestion) { if (jsonQuestion.TryGetProperty("name", out JsonElement jsonName)) _name = jsonName.GetString().TrimEnd('.'); if (jsonQuestion.TryGetProperty("blockZone", out JsonElement jsonBlockZone)) _blockZone = jsonBlockZone.GetBoolean(); if (jsonQuestion.TryGetProperty("type", out JsonElement jsonType)) { if (!Enum.TryParse(jsonType.GetString(), true, out DnsResourceRecordType type)) throw new NotSupportedException("DNS record type is not supported: " + jsonType.GetString()); _type = type; } else { _type = DnsResourceRecordType.Unknown; } } #endregion #region public public bool Matches(DnsQuestionRecord question) { if (_name is not null) { if (_blockZone) { if ((_name.Length > 0) && !_name.Equals(question.Name, StringComparison.OrdinalIgnoreCase) && !question.Name.EndsWith("." + _name, StringComparison.OrdinalIgnoreCase)) return false; } else { if (!_name.Equals(question.Name, StringComparison.OrdinalIgnoreCase)) return false; } } if ((_type != DnsResourceRecordType.Unknown) && (_type != question.Type)) return false; return true; } #endregion } } } ================================================ FILE: Apps/DropRequestsApp/DropRequestsApp.csproj ================================================  net9.0 false 7.0 false Technitium Technitium DNS Server Shreyas Zare DropRequestsApp DropRequests https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Drops incoming DNS requests that match list of blocked networks or blocked questions. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/DropRequestsApp/dnsApp.config ================================================ { "enableBlocking": true, "dropMalformedRequests": false, "allowedNetworks": [ "127.0.0.1", "::1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ], "blockedNetworks": [ ], "blockedQuestions": [ { "name": "example.com", "blockZone": true }, { "type": "ANY" }, { "name": "pizzaseo.com", "type": "RRSIG" }, { "name": "sl", "type": "ANY" }, { "name": "a.a.a.ooooops.space", "type": "A" } ] } ================================================ FILE: Apps/FailoverApp/Address.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Failover { enum FailoverType { Unknown = 0, Primary = 1, Secondary = 2 } public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler { #region variables HealthService _healthService; #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; if (_healthService is not null) _healthService.Dispose(); _disposed = true; } #endregion #region private private void GetAnswers(JsonElement jsonAddresses, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { switch (question.Type) { case DnsResourceRecordType.A: foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) { HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true); switch (response.Status) { case HealthStatus.Unknown: answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 10, new DnsARecordData(address))); break; case HealthStatus.Healthy: answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, appRecordTtl, new DnsARecordData(address))); break; } } } break; case DnsResourceRecordType.AAAA: foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) { HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, true); switch (response.Status) { case HealthStatus.Unknown: answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 10, new DnsAAAARecordData(address))); break; case HealthStatus.Healthy: answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, appRecordTtl, new DnsAAAARecordData(address))); break; } } } break; } } private void GetStatusAnswers(JsonElement jsonAddresses, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); HealthCheckResponse response = _healthService.QueryStatus(address, healthCheck, healthCheckUrl, false); string text = "app=failover; addressType=" + type.ToString() + "; address=" + address.ToString() + "; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";"; if (response.Status == HealthStatus.Failed) text += " failureReason=" + response.FailureReason + ";"; answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text))); } } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { if (_healthService is null) _healthService = HealthService.Create(dnsServer); _healthService.Initialize(config); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null); Uri healthCheckUrl = null; if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null)) { //read health check url only for http/https type checks and only when app config does not have an url configured if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null)) { healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString()); } else { if (hc.Type == HealthCheckType.Https) healthCheckUrl = new Uri("https://" + question.Name); else healthCheckUrl = new Uri("http://" + question.Name); } } List answers = new List(); if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) GetAnswers(jsonPrimary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); if (answers.Count == 0) { if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) GetAnswers(jsonSecondary, question, appRecordTtl, healthCheck, healthCheckUrl, answers); if (answers.Count == 0) { if (jsonAppRecordData.TryGetProperty("serverDown", out JsonElement jsonServerDown)) { if (question.Type == DnsResourceRecordType.A) { foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, 30, new DnsARecordData(address))); } } else { foreach (JsonElement jsonAddress in jsonServerDown.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, 30, new DnsAAAARecordData(address))); } } } if (answers.Count == 0) return Task.FromResult(null); } } if (answers.Count > 1) answers.Shuffle(); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); } case DnsResourceRecordType.TXT: { using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; bool allowTxtStatus = jsonAppRecordData.GetPropertyValue("allowTxtStatus", false); if (!allowTxtStatus) return Task.FromResult(null); string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null); Uri healthCheckUrl = null; if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null)) { //read health check url only for http/https type checks and only when app config does not have an url configured if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null)) { healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString()); } else { if (hc.Type == HealthCheckType.Https) healthCheckUrl = new Uri("https://" + question.Name); else healthCheckUrl = new Uri("http://" + question.Name); } } List answers = new List(); if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) GetStatusAnswers(jsonPrimary, FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, answers); if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) GetStatusAnswers(jsonSecondary, FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, answers); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); } default: return Task.FromResult(null); } } #endregion #region properties public string Description { get { return "Returns A or AAAA records from primary set of addresses with a continous health check as configured in the app config. When none of the primary addresses are healthy, the app returns healthy addresses from the secondary set of addresses. When none of the primary and secondary addresses are healthy, the app returns all addresses from the server down set of addresses. The server down feature is expected to be used for showing a service status page and not to serve the actual content.\n\nIf an URL is provided for the health check in the app's config then it will override the 'healthCheckUrl' parameter. When an URL is not provided in 'healthCheckUrl' parameter for 'http' or 'https' type health check, the domain name of the APP record will be used to auto generate an URL.\n\nSet 'allowTxtStatus' parameter to 'true' in your APP record data to allow checking health status by querying for TXT record."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""primary"": [ ""1.1.1.1"", ""::1"" ], ""secondary"": [ ""2.2.2.2"", ""::2"" ], ""serverDown"": [ ""3.3.3.3"" ], ""healthCheck"": ""https"", ""healthCheckUrl"": ""https://www.example.com/"", ""allowTxtStatus"": false }"; } } #endregion } } ================================================ FILE: Apps/FailoverApp/CNAME.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Failover { public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler { #region variables HealthService _healthService; #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; if (_healthService is not null) _healthService.Dispose(); _disposed = true; } #endregion #region private private DnsResourceRecord[] GetAnswers(string domain, DnsQuestionRecord question, string zoneName, uint appRecordTtl, string healthCheck, Uri healthCheckUrl) { DnsResourceRecordType healthCheckRecordType; if (question.Type == DnsResourceRecordType.AAAA) healthCheckRecordType = DnsResourceRecordType.AAAA; else healthCheckRecordType = DnsResourceRecordType.A; HealthCheckResponse response = _healthService.QueryStatus(domain, healthCheckRecordType, healthCheck, healthCheckUrl, true); switch (response.Status) { case HealthStatus.Unknown: if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 10, new DnsANAMERecordData(domain)) }; //use ANAME else return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 10, new DnsCNAMERecordData(domain)) }; case HealthStatus.Healthy: if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(domain)) }; //use ANAME else return new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(domain)) }; } return null; } private void GetStatusAnswers(string domain, FailoverType type, DnsQuestionRecord question, uint appRecordTtl, string healthCheck, Uri healthCheckUrl, List answers) { { HealthCheckResponse response = _healthService.QueryStatus(domain, DnsResourceRecordType.A, healthCheck, healthCheckUrl, false); string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: A; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";"; if (response.Status == HealthStatus.Failed) text += " failureReason=" + response.FailureReason + ";"; answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text))); } { HealthCheckResponse response = _healthService.QueryStatus(domain, DnsResourceRecordType.AAAA, healthCheck, healthCheckUrl, false); string text = "app=failover; cnameType=" + type.ToString() + "; domain=" + domain + "; qType: AAAA; healthCheck=" + healthCheck + (healthCheckUrl is null ? "" : "; healthCheckUrl=" + healthCheckUrl.AbsoluteUri) + "; healthStatus=" + response.Status.ToString() + ";"; if (response.Status == HealthStatus.Failed) text += " failureReason=" + response.FailureReason + ";"; answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, appRecordTtl, new DnsTXTRecordData(text))); } } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { if (_healthService is null) _healthService = HealthService.Create(dnsServer); //let Address class initialize config return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; string healthCheck = jsonAppRecordData.GetPropertyValue("healthCheck", null); Uri healthCheckUrl = null; if (_healthService.HealthChecks.TryGetValue(healthCheck, out HealthCheck hc) && ((hc.Type == HealthCheckType.Https) || (hc.Type == HealthCheckType.Http)) && (hc.Url is null)) { //read health check url only for http/https type checks and only when app config does not have an url configured if (jsonAppRecordData.TryGetProperty("healthCheckUrl", out JsonElement jsonHealthCheckUrl) && (jsonHealthCheckUrl.ValueKind != JsonValueKind.Null)) { healthCheckUrl = new Uri(jsonHealthCheckUrl.GetString()); } else { if (hc.Type == HealthCheckType.Https) healthCheckUrl = new Uri("https://" + question.Name); else healthCheckUrl = new Uri("http://" + question.Name); } } IReadOnlyList answers = null; if (question.Type == DnsResourceRecordType.TXT) { bool allowTxtStatus = jsonAppRecordData.GetPropertyValue("allowTxtStatus", false); if (!allowTxtStatus) return Task.FromResult(null); List txtAnswers = new List(); if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) GetStatusAnswers(jsonPrimary.GetString(), FailoverType.Primary, question, 30, healthCheck, healthCheckUrl, txtAnswers); if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) { foreach (JsonElement jsonDomain in jsonSecondary.EnumerateArray()) GetStatusAnswers(jsonDomain.GetString(), FailoverType.Secondary, question, 30, healthCheck, healthCheckUrl, txtAnswers); } answers = txtAnswers; } else { if (jsonAppRecordData.TryGetProperty("primary", out JsonElement jsonPrimary)) answers = GetAnswers(jsonPrimary.GetString(), question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); if (answers is null) { if (jsonAppRecordData.TryGetProperty("secondary", out JsonElement jsonSecondary)) { foreach (JsonElement jsonDomain in jsonSecondary.EnumerateArray()) { answers = GetAnswers(jsonDomain.GetString(), question, zoneName, appRecordTtl, healthCheck, healthCheckUrl); if (answers is not null) break; } } if (answers is null) { if (!jsonAppRecordData.TryGetProperty("serverDown", out JsonElement jsonServerDown) || (jsonServerDown.ValueKind == JsonValueKind.Null)) return Task.FromResult(null); string serverDown = jsonServerDown.GetString(); if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, 30, new DnsANAMERecordData(serverDown)) }; //use ANAME else answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, 30, new DnsCNAMERecordData(serverDown)) }; } } } return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); } #endregion #region properties public string Description { get { return "Returns CNAME record for primary domain name with a continous health check as configured in the app config. When the primary domain name is unhealthy, the app returns one of the secondary domain names in the given order of preference that is healthy. When none of the primary and secondary domain names are healthy, the app returns the server down domain name. The server down feature is expected to be used for showing a service status page and not to serve the actual content. Note that the app will return ANAME record for an APP record at zone apex.\n\nIf an URL is provided for the health check in the app's config then it will override the 'healthCheckUrl' parameter. When an URL is not provided in 'healthCheckUrl' parameter for 'http' or 'https' type health check, the domain name of the APP record will be used to auto generate an URL.\n\nSet 'allowTxtStatus' parameter to 'true' in your APP record data to allow checking health status by querying for TXT record."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""primary"": ""in.example.org"", ""secondary"": [ ""sg.example.org"", ""eu.example.org"" ], ""serverDown"": ""status.example.org"", ""healthCheck"": ""tcp443"", ""healthCheckUrl"": null, ""allowTxtStatus"": false }"; } } #endregion } } ================================================ FILE: Apps/FailoverApp/EmailAlert.cs ================================================ /* 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.ApplicationCommon; using System; using System.Net; using System.Net.Mail; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Mail; namespace Failover { class EmailAlert : IDisposable { #region variables readonly HealthService _service; readonly string _name; bool _enabled; MailAddress[] _alertTo; string _smtpServer; int _smtpPort; bool _startTls; bool _smtpOverTls; string _username; string _password; MailAddress _mailFrom; readonly SmtpClientEx _smtpClient; #endregion #region constructor public EmailAlert(HealthService service, JsonElement jsonEmailAlert) { _service = service; _smtpClient = new SmtpClientEx(); _smtpClient.DnsClient = new DnsClientInternal(_service.DnsServer); _name = jsonEmailAlert.GetPropertyValue("name", "default"); Reload(jsonEmailAlert); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_smtpClient is not null) _smtpClient.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region private private async Task SendMailAsync(MailMessage message) { try { const int MAX_RETRIES = 3; const int WAIT_INTERVAL = 30000; for (int retries = 0; retries < MAX_RETRIES; retries++) { try { await _smtpClient.SendMailAsync(message); break; } catch { if (retries == MAX_RETRIES - 1) throw; await Task.Delay(WAIT_INTERVAL); } } } catch (Exception ex) { _service.DnsServer.WriteLog("Failed to send email alert [" + _name + "].\r\n" + ex.ToString()); } } #endregion #region public public void Reload(JsonElement jsonEmailAlert) { _enabled = jsonEmailAlert.GetPropertyValue("enabled", false); if (jsonEmailAlert.TryReadArray("alertTo", delegate (string emailAddress) { return new MailAddress(emailAddress); }, out MailAddress[] alertTo)) _alertTo = alertTo; else _alertTo = null; _smtpServer = jsonEmailAlert.GetPropertyValue("smtpServer", null); _smtpPort = jsonEmailAlert.GetPropertyValue("smtpPort", 25); _startTls = jsonEmailAlert.GetPropertyValue("startTls", false); _smtpOverTls = jsonEmailAlert.GetPropertyValue("smtpOverTls", false); _username = jsonEmailAlert.GetPropertyValue("username", null); _password = jsonEmailAlert.GetPropertyValue("password", null); if (jsonEmailAlert.TryGetProperty("mailFrom", out JsonElement jsonMailFrom)) { if (jsonEmailAlert.TryGetProperty("mailFromName", out JsonElement jsonMailFromName)) _mailFrom = new MailAddress(jsonMailFrom.GetString(), jsonMailFromName.GetString(), Encoding.UTF8); else _mailFrom = new MailAddress(jsonMailFrom.GetString()); } else { _mailFrom = null; } //update smtp client settings _smtpClient.Host = _smtpServer; _smtpClient.Port = _smtpPort; _smtpClient.EnableSsl = _startTls; _smtpClient.SmtpOverTls = _smtpOverTls; if (string.IsNullOrEmpty(_username)) _smtpClient.Credentials = null; else _smtpClient.Credentials = new NetworkCredential(_username, _password); _smtpClient.Proxy = _service.DnsServer.Proxy; } public Task SendAlertAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse) { if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0)) return Task.CompletedTask; MailMessage message = new MailMessage(); message.From = _mailFrom; foreach (MailAddress alertTo in _alertTo) message.To.Add(alertTo); message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper(); switch (healthCheckResponse.Status) { case HealthStatus.Failed: message.Body = @"Hi, The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address failed to respond. Address: " + address.ToString() + @" Health Check: " + healthCheck + @" Status: " + healthCheckResponse.Status.ToString().ToUpper() + @" Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @" Failure Reason: " + healthCheckResponse.FailureReason + @" Regards, DNS Failover App "; break; default: message.Body = @"Hi, The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"] and found that the address status was " + healthCheckResponse.Status.ToString().ToUpper() + @". Address: " + address.ToString() + @" Health Check: " + healthCheck + @" Status: " + healthCheckResponse.Status.ToString().ToUpper() + @" Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @" Regards, DNS Failover App "; break; } return SendMailAsync(message); } public Task SendAlertAsync(IPAddress address, string healthCheck, Exception ex) { if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0)) return Task.CompletedTask; MailMessage message = new MailMessage(); message.From = _mailFrom; foreach (MailAddress alertTo in _alertTo) message.To.Add(alertTo); message.Subject = "[Alert] Address [" + address.ToString() + "] Status Is ERROR"; message.Body = @"Hi, The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the address [" + address.ToString() + @"]. Address: " + address.ToString() + @" Health Check: " + healthCheck + @" Status: ERROR Alert Time: " + DateTime.UtcNow.ToString("R") + @" Failure Reason: " + ex.ToString() + @" Regards, DNS Failover App "; return SendMailAsync(message); } public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse) { if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0)) return Task.CompletedTask; MailMessage message = new MailMessage(); message.From = _mailFrom; foreach (MailAddress alertTo in _alertTo) message.To.Add(alertTo); message.Subject = "[Alert] Domain [" + domain + "] Status Is " + healthCheckResponse.Status.ToString().ToUpper(); switch (healthCheckResponse.Status) { case HealthStatus.Failed: message.Body = @"Hi, The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name failed to respond. Domain: " + domain + @" Record Type: " + type.ToString() + @" Health Check: " + healthCheck + @" Status: " + healthCheckResponse.Status.ToString().ToUpper() + @" Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @" Failure Reason: " + healthCheckResponse.FailureReason + @" Regards, DNS Failover App "; break; default: message.Body = @"Hi, The DNS Failover App was successfully able to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"] and found that the domain name status was " + healthCheckResponse.Status.ToString().ToUpper() + @". Domain: " + domain + @" Record Type: " + type.ToString() + @" Health Check: " + healthCheck + @" Status: " + healthCheckResponse.Status.ToString().ToUpper() + @" Alert Time: " + healthCheckResponse.DateTime.ToString("R") + @" Regards, DNS Failover App "; break; } return SendMailAsync(message); } public Task SendAlertAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex) { if (!_enabled || (_mailFrom is null) || (_alertTo is null) || (_alertTo.Length == 0)) return Task.CompletedTask; MailMessage message = new MailMessage(); message.From = _mailFrom; foreach (MailAddress alertTo in _alertTo) message.To.Add(alertTo); message.Subject = "[Alert] Domain [" + domain + "] Status Is ERROR"; message.Body = @"Hi, The DNS Failover App has failed to perform a health check [" + healthCheck + "] on the domain name [" + domain + @"]. Domain: " + domain + @" Record Type: " + type.ToString() + @" Health Check: " + healthCheck + @" Status: ERROR Alert Time: " + DateTime.UtcNow.ToString("R") + @" Failure Reason: " + ex.ToString() + @" Regards, DNS Failover App "; return SendMailAsync(message); } #endregion #region properties public string Name { get { return _name; } } public bool Enabled { get { return _enabled; } } public MailAddress[] AlertTo { get { return _alertTo; } } public string SmtpServer { get { return _smtpServer; } } public int SmtpPort { get { return _smtpPort; } } public bool StartTls { get { return _startTls; } } public bool SmtpOverTls { get { return _smtpOverTls; } } public string Username { get { return _username; } } public string Password { get { return _password; } } public MailAddress MailFrom { get { return _mailFrom; } } #endregion class DnsClientInternal : IDnsClient { readonly IDnsServer _dnsServer; public DnsClientInternal(IDnsServer dnsServer) { _dnsServer = dnsServer; } public Task ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default) { return _dnsServer.DirectQueryAsync(question, cancellationToken: cancellationToken); } } } } ================================================ FILE: Apps/FailoverApp/FailoverApp.csproj ================================================  net9.0 false 9.1 false Technitium Technitium DNS Server Shreyas Zare FailoverApp Failover https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record based on the health status of the servers. The app supports email alerts and web hooks to relay the health status. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.IO.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.Mail.dll true PreserveNewest ================================================ FILE: Apps/FailoverApp/HealthCheck.cs ================================================ /* 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 System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Http.Client; using TechnitiumLibrary.Net.Proxy; namespace Failover { enum HealthCheckType { Unknown = 0, Ping = 1, Tcp = 2, Http = 3, Https = 4 } class HealthCheck : IDisposable { #region variables const string HTTP_HEALTH_CHECK_USER_AGENT = "DNS Failover App (Technitium DNS Server)"; readonly HealthService _service; readonly string _name; HealthCheckType _type; int _interval; int _retries; int _timeout; int _port; Uri _url; EmailAlert _emailAlert; WebHook _webHook; HttpClientNetworkHandler _httpHandler; HttpClient _httpClient; #endregion #region constructor public HealthCheck(HealthService service, JsonElement jsonHealthCheck) { _service = service; _name = jsonHealthCheck.GetPropertyValue("name", "default"); Reload(jsonHealthCheck); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_httpClient != null) { _httpClient.Dispose(); _httpClient = null; } if (_httpHandler != null) { _httpHandler.Dispose(); _httpHandler = null; } } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region private private void ConditionalHttpReload() { switch (_type) { case HealthCheckType.Http: case HealthCheckType.Https: bool handlerChanged = false; NetProxy proxy = _service.DnsServer.Proxy; if (_httpHandler is null) { HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler(); httpHandler.Proxy = proxy; httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; httpHandler.DnsClient = _service.DnsServer; httpHandler.InnerHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout); httpHandler.InnerHandler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(Math.Max(10000, _timeout)); httpHandler.InnerHandler.AllowAutoRedirect = false; _httpHandler = httpHandler; handlerChanged = true; } else { if ((_httpHandler.InnerHandler.ConnectTimeout.TotalMilliseconds != _timeout) || (_httpHandler.Proxy != proxy)) { HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler(); httpHandler.Proxy = proxy; httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; httpHandler.DnsClient = _service.DnsServer; httpHandler.InnerHandler.ConnectTimeout = TimeSpan.FromMilliseconds(_timeout); httpHandler.InnerHandler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(Math.Max(10000, _timeout)); httpHandler.InnerHandler.AllowAutoRedirect = false; HttpClientNetworkHandler oldHttpHandler = _httpHandler; _httpHandler = httpHandler; handlerChanged = true; oldHttpHandler.Dispose(); } } if (_httpClient is null) { HttpClient httpClient = new HttpClient(_httpHandler); httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(HTTP_HEALTH_CHECK_USER_AGENT); httpClient.DefaultRequestHeaders.ConnectionClose = true; _httpClient = httpClient; } else { if (handlerChanged || (_httpClient.Timeout.TotalMilliseconds != _timeout)) { HttpClient httpClient = new HttpClient(_httpHandler); httpClient.Timeout = TimeSpan.FromMilliseconds(_timeout); httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(HTTP_HEALTH_CHECK_USER_AGENT); httpClient.DefaultRequestHeaders.ConnectionClose = true; HttpClient oldHttpClient = _httpClient; _httpClient = httpClient; oldHttpClient.Dispose(); } } break; default: if (_httpClient != null) { _httpClient.Dispose(); _httpClient = null; } if (_httpHandler != null) { _httpHandler.Dispose(); _httpHandler = null; } break; } } #endregion #region public public void Reload(JsonElement jsonHealthCheck) { _type = Enum.Parse(jsonHealthCheck.GetPropertyValue("type", "Tcp"), true); _interval = jsonHealthCheck.GetPropertyValue("interval", 60) * 1000; _retries = jsonHealthCheck.GetPropertyValue("retries", 3); _timeout = jsonHealthCheck.GetPropertyValue("timeout", 10) * 1000; _port = jsonHealthCheck.GetPropertyValue("port", 80); if (jsonHealthCheck.TryGetProperty("url", out JsonElement jsonUrl) && (jsonUrl.ValueKind != JsonValueKind.Null)) _url = new Uri(jsonUrl.GetString()); else _url = null; if (jsonHealthCheck.TryGetProperty("emailAlert", out JsonElement jsonEmailAlert) && _service.EmailAlerts.TryGetValue(jsonEmailAlert.GetString(), out EmailAlert emailAlert)) _emailAlert = emailAlert; else _emailAlert = null; if (jsonHealthCheck.TryGetProperty("webHook", out JsonElement jsonWebHook) && _service.WebHooks.TryGetValue(jsonWebHook.GetString(), out WebHook webHook)) _webHook = webHook; else _webHook = null; ConditionalHttpReload(); } public async Task IsHealthyAsync(string domain, DnsResourceRecordType type, Uri healthCheckUrl) { switch (type) { case DnsResourceRecordType.A: { DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN)); if ((response is null) || (response.Answer.Count == 0)) return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address."); IReadOnlyList addresses = DnsClient.ParseResponseA(response); if (addresses.Count > 0) { HealthCheckResponse lastResponse = null; foreach (IPAddress address in addresses) { lastResponse = await IsHealthyAsync(address, healthCheckUrl); if (lastResponse.Status == HealthStatus.Healthy) return lastResponse; } return lastResponse; } return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address."); } case DnsResourceRecordType.AAAA: { DnsDatagram response = await _service.DnsServer.DirectQueryAsync(new DnsQuestionRecord(domain, type, DnsClass.IN)); if ((response is null) || (response.Answer.Count == 0)) return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address."); IReadOnlyList addresses = DnsClient.ParseResponseAAAA(response); if (addresses.Count > 0) { HealthCheckResponse lastResponse = null; foreach (IPAddress address in addresses) { lastResponse = await IsHealthyAsync(address, healthCheckUrl); if (lastResponse.Status == HealthStatus.Healthy) return lastResponse; } return lastResponse; } return new HealthCheckResponse(HealthStatus.Failed, "Failed to resolve address."); } default: return new HealthCheckResponse(HealthStatus.Failed, "Not supported."); } } public async Task IsHealthyAsync(IPAddress address, Uri healthCheckUrl) { foreach (KeyValuePair network in _service.UnderMaintenance) { if (network.Key.Contains(address)) { if (network.Value) return new HealthCheckResponse(HealthStatus.Maintenance); break; } } switch (_type) { case HealthCheckType.Ping: { if (_service.DnsServer.Proxy != null) throw new NotSupportedException("Health check type 'ping' is not supported over proxy."); using (Ping ping = new Ping()) { string lastReason; int retry = 0; do { PingReply reply = await ping.SendPingAsync(address, _timeout); if (reply.Status == IPStatus.Success) return new HealthCheckResponse(HealthStatus.Healthy); lastReason = reply.Status.ToString(); } while (++retry < _retries); return new HealthCheckResponse(HealthStatus.Failed, lastReason); } } case HealthCheckType.Tcp: { Exception lastException; string lastReason = null; int retry = 0; do { try { NetProxy proxy = _service.DnsServer.Proxy; if (proxy is null) { using (Socket socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) { await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return socket.ConnectAsync(address, _port, cancellationToken1).AsTask(); }, _timeout); } } else { using (Socket socket = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return proxy.ConnectAsync(new IPEndPoint(address, _port), cancellationToken1); }, _timeout)) { //do nothing } } return new HealthCheckResponse(HealthStatus.Healthy); } catch (TimeoutException ex) { lastReason = "Connection timed out."; lastException = ex; } catch (SocketException ex) { lastReason = ex.Message; lastException = ex; } catch (Exception ex) { lastException = ex; } } while (++retry < _retries); return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException); } case HealthCheckType.Http: case HealthCheckType.Https: { ConditionalHttpReload(); Exception lastException; string lastReason = null; int retry = 0; do { try { Uri url; if (_url is null) url = healthCheckUrl; else url = _url; if (url is null) return new HealthCheckResponse(HealthStatus.Failed, "Missing health check URL in APP record as well as in app config."); if (_type == HealthCheckType.Http) { if (url.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) url = new Uri("http://" + url.Host + (url.IsDefaultPort ? "" : ":" + url.Port) + url.PathAndQuery); } else { if (url.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) url = new Uri("https://" + url.Host + (url.IsDefaultPort ? "" : ":" + url.Port) + url.PathAndQuery); } IPEndPoint ep = new IPEndPoint(address, url.Port); Uri queryUri = new Uri(url.Scheme + "://" + ep.ToString() + url.PathAndQuery); HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, queryUri); if (url.IsDefaultPort) httpRequest.Headers.Host = url.Host; else httpRequest.Headers.Host = url.Host + ":" + url.Port; HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest); if (httpResponse.IsSuccessStatusCode) return new HealthCheckResponse(HealthStatus.Healthy); return new HealthCheckResponse(HealthStatus.Failed, "Received HTTP status code: " + (int)httpResponse.StatusCode + " " + httpResponse.StatusCode.ToString() + "; URL: " + url.AbsoluteUri); } catch (OperationCanceledException ex) { lastReason = "Connection timed out."; lastException = ex; } catch (HttpRequestException ex) { lastReason = ex.Message; lastException = ex; } catch (Exception ex) { lastException = ex; } } while (++retry < _retries); return new HealthCheckResponse(HealthStatus.Failed, lastReason, lastException); } default: throw new NotSupportedException(); } } #endregion #region properties public string Name { get { return _name; } } public HealthCheckType Type { get { return _type; } } public int Interval { get { return _interval; } } public int Retries { get { return _retries; } } public int Timeout { get { return _timeout; } } public int Port { get { return _port; } } public Uri Url { get { return _url; } } public EmailAlert EmailAlert { get { return _emailAlert; } } public WebHook WebHook { get { return _webHook; } } #endregion } } ================================================ FILE: Apps/FailoverApp/HealthCheckResponse.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 System; namespace Failover { enum HealthStatus { Unknown = 0, Failed = 1, Healthy = 2, Maintenance = 3 } class HealthCheckResponse { #region variables public readonly DateTime DateTime = DateTime.UtcNow; public readonly HealthStatus Status; public readonly string FailureReason; public readonly Exception Exception; #endregion #region constructor public HealthCheckResponse(HealthStatus status, string failureReason = null, Exception exception = null) { Status = status; FailureReason = failureReason; Exception = exception; } #endregion } } ================================================ FILE: Apps/FailoverApp/HealthMonitor.cs ================================================ /* 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.ApplicationCommon; using System; using System.Net; using System.Threading; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Failover { class HealthMonitor : IDisposable { #region variables readonly IDnsServer _dnsServer; readonly IPAddress _address; readonly string _domain; readonly DnsResourceRecordType _type; readonly HealthCheck _healthCheck; readonly Timer _healthCheckTimer; const int HEALTH_CHECK_TIMER_INITIAL_INTERVAL = 1000; HealthCheckResponse _lastHealthCheckResponse; const int MONITOR_EXPIRY = 1 * 60 * 60 * 1000; //1 hour DateTime _lastHealthStatusCheckedOn; #endregion #region constructor public HealthMonitor(IDnsServer dnsServer, IPAddress address, HealthCheck healthCheck, Uri healthCheckUrl) { _dnsServer = dnsServer; _address = address; _healthCheck = healthCheck; _healthCheckTimer = new Timer(async delegate (object state) { try { if (_healthCheck is null) { _lastHealthCheckResponse = null; } else { HealthCheckResponse healthCheckResponse = await _healthCheck.IsHealthyAsync(_address, healthCheckUrl); bool statusChanged = false; bool maintenance = false; if (_lastHealthCheckResponse is null) { switch (healthCheckResponse.Status) { case HealthStatus.Failed: statusChanged = true; break; case HealthStatus.Maintenance: statusChanged = true; maintenance = true; break; } } else { if (_lastHealthCheckResponse.Status != healthCheckResponse.Status) { statusChanged = true; if ((_lastHealthCheckResponse.Status == HealthStatus.Maintenance) || (healthCheckResponse.Status == HealthStatus.Maintenance)) maintenance = true; } } if (statusChanged) { switch (healthCheckResponse.Status) { case HealthStatus.Failed: _dnsServer.WriteLog("ALERT! Address [" + _address.ToString() + "] status is FAILED based on '" + _healthCheck.Name + "' health check. The failure reason is: " + healthCheckResponse.FailureReason); break; default: _dnsServer.WriteLog("ALERT! Address [" + _address.ToString() + "] status is " + healthCheckResponse.Status.ToString().ToUpper() + " based on '" + _healthCheck.Name + "' health check."); break; } if (healthCheckResponse.Exception is not null) _dnsServer.WriteLog(healthCheckResponse.Exception); if (!maintenance) { //avoid sending email alerts when switching from or to maintenance EmailAlert emailAlert = _healthCheck.EmailAlert; if (emailAlert is not null) _ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, healthCheckResponse); } WebHook webHook = _healthCheck.WebHook; if (webHook is not null) _ = webHook.CallAsync(_address, _healthCheck.Name, healthCheckResponse); } _lastHealthCheckResponse = healthCheckResponse; } } catch (Exception ex) { _dnsServer.WriteLog(ex); if (_lastHealthCheckResponse is null) { EmailAlert emailAlert = _healthCheck.EmailAlert; if (emailAlert is not null) _ = emailAlert.SendAlertAsync(_address, _healthCheck.Name, ex); WebHook webHook = _healthCheck.WebHook; if (webHook is not null) _ = webHook.CallAsync(_address, _healthCheck.Name, ex); _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Failed, ex.ToString(), ex); } else { _lastHealthCheckResponse = null; } } finally { if (!_disposed && (_healthCheck is not null)) _healthCheckTimer.Change(_healthCheck.Interval, Timeout.Infinite); } }, null, Timeout.Infinite, Timeout.Infinite); _healthCheckTimer.Change(HEALTH_CHECK_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } public HealthMonitor(IDnsServer dnsServer, string domain, DnsResourceRecordType type, HealthCheck healthCheck, Uri healthCheckUrl) { _dnsServer = dnsServer; _domain = domain; _type = type; _healthCheck = healthCheck; _healthCheckTimer = new Timer(async delegate (object state) { try { if (_healthCheck is null) { _lastHealthCheckResponse = null; } else { HealthCheckResponse healthCheckResponse = await _healthCheck.IsHealthyAsync(_domain, _type, healthCheckUrl); bool statusChanged = false; bool maintenance = false; if (_lastHealthCheckResponse is null) { switch (healthCheckResponse.Status) { case HealthStatus.Failed: statusChanged = true; break; case HealthStatus.Maintenance: statusChanged = true; maintenance = true; break; } } else { if (_lastHealthCheckResponse.Status != healthCheckResponse.Status) { statusChanged = true; if ((_lastHealthCheckResponse.Status == HealthStatus.Maintenance) || (healthCheckResponse.Status == HealthStatus.Maintenance)) maintenance = true; } } if (statusChanged) { switch (healthCheckResponse.Status) { case HealthStatus.Failed: _dnsServer.WriteLog("ALERT! Domain [" + _domain + "] type [" + _type.ToString() + "] status is FAILED based on '" + _healthCheck.Name + "' health check. The failure reason is: " + healthCheckResponse.FailureReason); break; default: _dnsServer.WriteLog("ALERT! Domain [" + _domain + "] type [" + _type.ToString() + "] status is " + healthCheckResponse.Status.ToString().ToUpper() + " based on '" + _healthCheck.Name + "' health check."); break; } if (healthCheckResponse.Exception is not null) _dnsServer.WriteLog(healthCheckResponse.Exception); if (!maintenance) { //avoid sending email alerts when switching from or to maintenance EmailAlert emailAlert = _healthCheck.EmailAlert; if (emailAlert is not null) _ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, healthCheckResponse); } WebHook webHook = _healthCheck.WebHook; if (webHook is not null) _ = webHook.CallAsync(_domain, _type, _healthCheck.Name, healthCheckResponse); } _lastHealthCheckResponse = healthCheckResponse; } } catch (Exception ex) { _dnsServer.WriteLog(ex); if (_lastHealthCheckResponse is null) { EmailAlert emailAlert = _healthCheck.EmailAlert; if (emailAlert is not null) _ = emailAlert.SendAlertAsync(_domain, _type, _healthCheck.Name, ex); WebHook webHook = _healthCheck.WebHook; if (webHook is not null) _ = webHook.CallAsync(_domain, _type, _healthCheck.Name, ex); _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Failed, ex.ToString(), ex); } else { _lastHealthCheckResponse = null; } } finally { try { _healthCheckTimer?.Change(_healthCheck.Interval, Timeout.Infinite); } catch (ObjectDisposedException) { } } }, null, Timeout.Infinite, Timeout.Infinite); _healthCheckTimer.Change(HEALTH_CHECK_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _healthCheckTimer?.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region public public bool IsExpired() { return DateTime.UtcNow > _lastHealthStatusCheckedOn.AddMilliseconds(MONITOR_EXPIRY); } public void SetUnderMaintenance() { _lastHealthCheckResponse = new HealthCheckResponse(HealthStatus.Maintenance); } #endregion #region properties public IPAddress Address { get { return _address; } } public HealthCheckResponse LastHealthCheckResponse { get { _lastHealthStatusCheckedOn = DateTime.UtcNow; if (_lastHealthCheckResponse is null) return new HealthCheckResponse(HealthStatus.Unknown); return _lastHealthCheckResponse; } } #endregion } } ================================================ FILE: Apps/FailoverApp/HealthService.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace Failover { class HealthService : IDisposable { #region variables static HealthService _healthService; readonly IDnsServer _dnsServer; readonly ConcurrentDictionary _healthChecks = new ConcurrentDictionary(1, 5); readonly ConcurrentDictionary _emailAlerts = new ConcurrentDictionary(1, 2); readonly ConcurrentDictionary _webHooks = new ConcurrentDictionary(1, 2); readonly ConcurrentDictionary _underMaintenance = new ConcurrentDictionary(); readonly ConcurrentDictionary _healthMonitors = new ConcurrentDictionary(); readonly Timer _maintenanceTimer; const int MAINTENANCE_TIMER_INTERVAL = 15 * 60 * 1000; //15 mins #endregion #region constructor private HealthService(IDnsServer dnsServer) { _dnsServer = dnsServer; _maintenanceTimer = new Timer(delegate (object state) { try { foreach (KeyValuePair healthMonitor in _healthMonitors) { if (healthMonitor.Value.IsExpired()) { if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor)) removedMonitor.Dispose(); } } } catch (Exception ex) { _dnsServer.WriteLog(ex); } finally { try { _maintenanceTimer?.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } }, null, Timeout.Infinite, Timeout.Infinite); _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _maintenanceTimer?.Dispose(); foreach (KeyValuePair healthCheck in _healthChecks) healthCheck.Value.Dispose(); _healthChecks.Clear(); foreach (KeyValuePair emailAlert in _emailAlerts) emailAlert.Value.Dispose(); _emailAlerts.Clear(); foreach (KeyValuePair webHook in _webHooks) webHook.Value.Dispose(); _webHooks.Clear(); foreach (KeyValuePair healthMonitor in _healthMonitors) healthMonitor.Value.Dispose(); _healthMonitors.Clear(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region static public static HealthService Create(IDnsServer dnsServer) { if (_healthService is null) _healthService = new HealthService(dnsServer); return _healthService; } #endregion #region private private static string GetHealthMonitorKey(IPAddress address, string healthCheck, Uri healthCheckUrl) { //key: health-check|127.0.0.1 //key: health-check|127.0.0.1|http://example.com/ if (healthCheckUrl is null) return healthCheck + "|" + address.ToString(); else return healthCheck + "|" + address.ToString() + "|" + healthCheckUrl.AbsoluteUri; } private static string GetHealthMonitorKey(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl) { //key: health-check|example.com|A //key: health-check|example.com|AAAA|http://example.com/ if (healthCheckUrl is null) return healthCheck + "|" + domain + "|" + type.ToString(); else return healthCheck + "|" + domain + "|" + type.ToString() + "|" + healthCheckUrl.AbsoluteUri; } private void RemoveHealthMonitor(string healthCheck) { foreach (KeyValuePair healthMonitor in _healthMonitors) { if (healthMonitor.Key.StartsWith(healthCheck + "|")) { if (_healthMonitors.TryRemove(healthMonitor.Key, out HealthMonitor removedMonitor)) removedMonitor.Dispose(); } } } #endregion #region public public void Initialize(string config) { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; //email alerts { JsonElement jsonEmailAlerts = jsonConfig.GetProperty("emailAlerts"); //add or update email alerts foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray()) { string name = jsonEmailAlert.GetPropertyValue("name", "default"); if (_emailAlerts.TryGetValue(name, out EmailAlert existingEmailAlert)) { //update existingEmailAlert.Reload(jsonEmailAlert); } else { //add EmailAlert emailAlert = new EmailAlert(this, jsonEmailAlert); _emailAlerts.TryAdd(emailAlert.Name, emailAlert); } } //remove email alerts that dont exists in config foreach (KeyValuePair emailAlert in _emailAlerts) { bool emailAlertExists = false; foreach (JsonElement jsonEmailAlert in jsonEmailAlerts.EnumerateArray()) { string name = jsonEmailAlert.GetPropertyValue("name", "default"); if (name == emailAlert.Key) { emailAlertExists = true; break; } } if (!emailAlertExists) { if (_emailAlerts.TryRemove(emailAlert.Key, out EmailAlert removedEmailAlert)) removedEmailAlert.Dispose(); } } } //web hooks { JsonElement jsonWebHooks = jsonConfig.GetProperty("webHooks"); //add or update email alerts foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray()) { string name = jsonWebHook.GetPropertyValue("name", "default"); if (_webHooks.TryGetValue(name, out WebHook existingWebHook)) { //update existingWebHook.Reload(jsonWebHook); } else { //add WebHook webHook = new WebHook(this, jsonWebHook); _webHooks.TryAdd(webHook.Name, webHook); } } //remove email alerts that dont exists in config foreach (KeyValuePair webHook in _webHooks) { bool webHookExists = false; foreach (JsonElement jsonWebHook in jsonWebHooks.EnumerateArray()) { string name = jsonWebHook.GetPropertyValue("name", "default"); if (name == webHook.Key) { webHookExists = true; break; } } if (!webHookExists) { if (_webHooks.TryRemove(webHook.Key, out WebHook removedWebHook)) removedWebHook.Dispose(); } } } //health checks { JsonElement jsonHealthChecks = jsonConfig.GetProperty("healthChecks"); //add or update health checks foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray()) { string name = jsonHealthCheck.GetPropertyValue("name", "default"); if (_healthChecks.TryGetValue(name, out HealthCheck existingHealthCheck)) { //update existingHealthCheck.Reload(jsonHealthCheck); } else { //add HealthCheck healthCheck = new HealthCheck(this, jsonHealthCheck); _healthChecks.TryAdd(healthCheck.Name, healthCheck); } } //remove health checks that dont exists in config foreach (KeyValuePair healthCheck in _healthChecks) { bool healthCheckExists = false; foreach (JsonElement jsonHealthCheck in jsonHealthChecks.EnumerateArray()) { string name = jsonHealthCheck.GetPropertyValue("name", "default"); if (name == healthCheck.Key) { healthCheckExists = true; break; } } if (!healthCheckExists) { if (_healthChecks.TryRemove(healthCheck.Key, out HealthCheck removedHealthCheck)) { //remove health monitors using this health check RemoveHealthMonitor(healthCheck.Key); removedHealthCheck.Dispose(); } } } } //under maintenance networks _underMaintenance.Clear(); if (jsonConfig.TryGetProperty("underMaintenance", out JsonElement jsonUnderMaintenance)) { foreach (JsonElement jsonNetwork in jsonUnderMaintenance.EnumerateArray()) { string network = jsonNetwork.GetProperty("network").GetString(); bool enabled; if (jsonNetwork.TryGetProperty("enabled", out JsonElement jsonEnabled)) enabled = jsonEnabled.GetBoolean(); else if (jsonNetwork.TryGetProperty("enable", out JsonElement jsonEnable)) enabled = jsonEnable.GetBoolean(); else enabled = true; NetworkAddress umNetwork = NetworkAddress.Parse(network); if (_underMaintenance.TryAdd(umNetwork, enabled)) { if (enabled) { foreach (KeyValuePair healthMonitor in _healthMonitors) { HealthMonitor monitor = healthMonitor.Value; if (monitor.Address is null) continue; if (umNetwork.Contains(monitor.Address)) monitor.SetUnderMaintenance(); } } } } } } public HealthCheckResponse QueryStatus(IPAddress address, string healthCheck, Uri healthCheckUrl, bool tryAdd) { string healthMonitorKey = GetHealthMonitorKey(address, healthCheck, healthCheckUrl); if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor)) return monitor.LastHealthCheckResponse; if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck)) { if (tryAdd) { monitor = new HealthMonitor(_dnsServer, address, existingHealthCheck, healthCheckUrl); if (!_healthMonitors.TryAdd(healthMonitorKey, monitor)) monitor.Dispose(); //failed to add first } return new HealthCheckResponse(HealthStatus.Unknown); } else { return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck); } } public HealthCheckResponse QueryStatus(string domain, DnsResourceRecordType type, string healthCheck, Uri healthCheckUrl, bool tryAdd) { domain = domain.ToLowerInvariant(); string healthMonitorKey = GetHealthMonitorKey(domain, type, healthCheck, healthCheckUrl); if (_healthMonitors.TryGetValue(healthMonitorKey, out HealthMonitor monitor)) return monitor.LastHealthCheckResponse; if (_healthChecks.TryGetValue(healthCheck, out HealthCheck existingHealthCheck)) { if (tryAdd) { monitor = new HealthMonitor(_dnsServer, domain, type, existingHealthCheck, healthCheckUrl); if (!_healthMonitors.TryAdd(healthMonitorKey, monitor)) monitor.Dispose(); //failed to add first } return new HealthCheckResponse(HealthStatus.Unknown); } else { return new HealthCheckResponse(HealthStatus.Failed, "No such health check: " + healthCheck); } } #endregion #region properties public ConcurrentDictionary HealthChecks { get { return _healthChecks; } } public ConcurrentDictionary EmailAlerts { get { return _emailAlerts; } } public ConcurrentDictionary WebHooks { get { return _webHooks; } } public ConcurrentDictionary UnderMaintenance { get { return _underMaintenance; } } public IDnsServer DnsServer { get { return _dnsServer; } } #endregion } } ================================================ FILE: Apps/FailoverApp/WebHook.cs ================================================ /* 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 System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Http.Client; using TechnitiumLibrary.Net.Proxy; namespace Failover { class WebHook : IDisposable { #region variables readonly HealthService _service; readonly string _name; bool _enabled; Uri[] _urls; HttpClientNetworkHandler _httpHandler; HttpClient _httpClient; #endregion #region constructor public WebHook(HealthService service, JsonElement jsonWebHook) { _service = service; _name = jsonWebHook.GetPropertyValue("name", "default"); Reload(jsonWebHook); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_httpClient != null) { _httpClient.Dispose(); _httpClient = null; } if (_httpHandler != null) { _httpHandler.Dispose(); _httpHandler = null; } } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region private private void ConditionalHttpReload() { bool handlerChanged = false; NetProxy proxy = _service.DnsServer.Proxy; if (_httpHandler is null) { HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler(); httpHandler.Proxy = proxy; httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; httpHandler.DnsClient = _service.DnsServer; httpHandler.InnerHandler.AllowAutoRedirect = true; httpHandler.InnerHandler.MaxAutomaticRedirections = 10; _httpHandler = httpHandler; handlerChanged = true; } else { if (_httpHandler.Proxy != proxy) { HttpClientNetworkHandler httpHandler = new HttpClientNetworkHandler(); httpHandler.Proxy = proxy; httpHandler.NetworkType = _service.DnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; httpHandler.DnsClient = _service.DnsServer; httpHandler.InnerHandler.AllowAutoRedirect = true; httpHandler.InnerHandler.MaxAutomaticRedirections = 10; HttpClientNetworkHandler oldHttpHandler = _httpHandler; _httpHandler = httpHandler; handlerChanged = true; oldHttpHandler.Dispose(); } } if (_httpClient is null) { HttpClient httpClient = new HttpClient(_httpHandler); _httpClient = httpClient; } else { if (handlerChanged) { HttpClient httpClient = new HttpClient(_httpHandler); HttpClient oldHttpClient = _httpClient; _httpClient = httpClient; oldHttpClient.Dispose(); } } } private async Task CallAsync(HttpContent content) { ConditionalHttpReload(); async Task CallWebHook(Uri url) { try { HttpResponseMessage response = await _httpClient.PostAsync(url, content); response.EnsureSuccessStatusCode(); } catch (Exception ex) { _service.DnsServer.WriteLog("Webhook call failed for URL: " + url.AbsoluteUri + "\r\n" + ex.ToString()); } } List tasks = new List(); foreach (Uri url in _urls) tasks.Add(CallWebHook(url)); await Task.WhenAll(tasks); } #endregion #region public public void Reload(JsonElement jsonWebHook) { _enabled = jsonWebHook.GetPropertyValue("enabled", false); if (jsonWebHook.TryReadArray("urls", delegate (string uri) { return new Uri(uri); }, out Uri[] urls)) _urls = urls; else _urls = null; ConditionalHttpReload(); } public Task CallAsync(IPAddress address, string healthCheck, HealthCheckResponse healthCheckResponse) { if (!_enabled) return Task.CompletedTask; HttpContent content; { using (MemoryStream mS = new MemoryStream()) { Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); jsonWriter.WriteString("address", address.ToString()); jsonWriter.WriteString("healthCheck", healthCheck); jsonWriter.WriteString("status", healthCheckResponse.Status.ToString()); if (healthCheckResponse.Status == HealthStatus.Failed) jsonWriter.WriteString("failureReason", healthCheckResponse.FailureReason); jsonWriter.WriteString("dateTime", healthCheckResponse.DateTime); jsonWriter.WriteEndObject(); jsonWriter.Flush(); content = new ByteArrayContent(mS.ToArray()); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } } return CallAsync(content); } public Task CallAsync(IPAddress address, string healthCheck, Exception ex) { if (!_enabled) return Task.CompletedTask; HttpContent content; { using (MemoryStream mS = new MemoryStream()) { Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); jsonWriter.WriteString("address", address.ToString()); jsonWriter.WriteString("healthCheck", healthCheck); jsonWriter.WriteString("status", "Error"); jsonWriter.WriteString("failureReason", ex.ToString()); jsonWriter.WriteString("dateTime", DateTime.UtcNow); jsonWriter.WriteEndObject(); jsonWriter.Flush(); content = new ByteArrayContent(mS.ToArray()); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } } return CallAsync(content); } public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, HealthCheckResponse healthCheckResponse) { if (!_enabled) return Task.CompletedTask; HttpContent content; { using (MemoryStream mS = new MemoryStream()) { Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); jsonWriter.WriteString("domain", domain); jsonWriter.WriteString("recordType", type.ToString()); jsonWriter.WriteString("healthCheck", healthCheck); jsonWriter.WriteString("status", healthCheckResponse.Status.ToString()); if (healthCheckResponse.Status == HealthStatus.Failed) jsonWriter.WriteString("failureReason", healthCheckResponse.FailureReason); jsonWriter.WriteString("dateTime", healthCheckResponse.DateTime); jsonWriter.WriteEndObject(); jsonWriter.Flush(); content = new ByteArrayContent(mS.ToArray()); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } } return CallAsync(content); } public Task CallAsync(string domain, DnsResourceRecordType type, string healthCheck, Exception ex) { if (!_enabled) return Task.CompletedTask; HttpContent content; { using (MemoryStream mS = new MemoryStream()) { Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); jsonWriter.WriteString("domain", domain); jsonWriter.WriteString("recordType", type.ToString()); jsonWriter.WriteString("healthCheck", healthCheck); jsonWriter.WriteString("status", "Error"); jsonWriter.WriteString("failureReason", ex.ToString()); jsonWriter.WriteString("dateTime", DateTime.UtcNow); jsonWriter.WriteEndObject(); jsonWriter.Flush(); content = new ByteArrayContent(mS.ToArray()); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } } return CallAsync(content); } #endregion #region properties public string Name { get { return _name; } } public bool Enabled { get { return _enabled; } } public Uri[] Urls { get { return _urls; } } #endregion } } ================================================ FILE: Apps/FailoverApp/dnsApp.config ================================================ { "healthChecks": [ { "name": "ping", "type": "ping", "interval": 60, "retries": 3, "timeout": 10, "emailAlert": "default", "webHook": "default" }, { "name": "tcp80", "type": "tcp", "interval": 60, "retries": 3, "timeout": 10, "port": 80, "emailAlert": "default", "webHook": "default" }, { "name": "tcp443", "type": "tcp", "interval": 60, "retries": 3, "timeout": 10, "port": 443, "emailAlert": "default", "webHook": "default" }, { "name": "http", "type": "http", "interval": 60, "retries": 3, "timeout": 10, "url": null, "emailAlert": "default", "webHook": "default" }, { "name": "https", "type": "https", "interval": 60, "retries": 3, "timeout": 10, "url": null, "emailAlert": "default", "webHook": "default" }, { "name": "www.example.com", "type": "https", "interval": 60, "retries": 3, "timeout": 10, "url": "https://www.example.com", "emailAlert": "default", "webHook": "default" } ], "emailAlerts": [ { "name": "default", "enabled": false, "alertTo": [ "admin@example.com" ], "smtpServer": "smtp.example.com", "smtpPort": 465, "startTls": false, "smtpOverTls": true, "username": "alerts@example.com", "password": "password", "mailFrom": "alerts@example.com", "mailFromName": "DNS Server Alert" } ], "webHooks": [ { "name": "default", "enabled": false, "urls": [ "https://webhooks.example.com/default" ] } ], "underMaintenance": [ { "network": "192.168.10.2/32", "enabled": false }, { "network": "10.1.1.0/24", "enabled": false } ] } ================================================ FILE: Apps/FilterAaaaApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace FilterAaaa { public sealed class App : IDnsApplication, IDnsPostProcessor { #region variables IDnsServer _dnsServer; bool _enableFilterAaaa; uint _defaultTtl; bool _bypassLocalZones; NetworkAddress[] _bypassNetworks; string[] _bypassDomains; string[] _filterDomains; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _enableFilterAaaa = jsonConfig.GetPropertyValue("enableFilterAaaa", false); if (jsonConfig.TryGetProperty("defaultTtl", out JsonElement jsonValue)) { if (!jsonValue.TryGetUInt32(out _defaultTtl)) _defaultTtl = 30u; } else { _defaultTtl = 30u; //update config for new option config = config.Replace("\"bypassLocalZones\"", "\"defaultTtl\": 30,\r\n \"bypassLocalZones\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } _bypassLocalZones = jsonConfig.GetPropertyValue("bypassLocalZones", false); if (jsonConfig.TryReadArray("bypassNetworks", NetworkAddress.Parse, out NetworkAddress[] bypassNetworks)) _bypassNetworks = bypassNetworks; else _bypassNetworks = []; if (jsonConfig.TryReadArray("bypassDomains", out string[] bypassDomains)) _bypassDomains = bypassDomains; else _bypassDomains = []; if (jsonConfig.TryReadArray("filterDomains", out string[] filterDomains)) { _filterDomains = filterDomains; } else { _filterDomains = []; //update config for new feature config = config.TrimEnd('\r', '\n', ' ', '}'); config += ",\r\n \"filterDomains\": [\r\n ]\r\n}"; await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } } public async Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (!_enableFilterAaaa) return response; if (_bypassLocalZones && response.AuthoritativeAnswer) return response; if (response.RCODE != DnsResponseCode.NoError) return response; DnsQuestionRecord question = request.Question[0]; if (question.Type != DnsResourceRecordType.AAAA) return response; bool hasAAAA = false; if (request.DnssecOk) { foreach (DnsResourceRecord record in response.Answer) { switch (record.Type) { case DnsResourceRecordType.AAAA: hasAAAA = true; break; case DnsResourceRecordType.RRSIG: //response is signed and the client is DNSSEC aware; must not be modified return response; } } } else { foreach (DnsResourceRecord record in response.Answer) { if (record.Type == DnsResourceRecordType.AAAA) { hasAAAA = true; break; } } } if (!hasAAAA) return response; IPAddress remoteIP = remoteEP.Address; foreach (NetworkAddress network in _bypassNetworks) { if (network.Contains(remoteIP)) return response; } string qname = question.Name; foreach (string allowedDomain in _bypassDomains) { if (qname.Equals(allowedDomain, StringComparison.OrdinalIgnoreCase) || qname.EndsWith("." + allowedDomain, StringComparison.OrdinalIgnoreCase)) return response; } bool filterDomain = _filterDomains.Length == 0; foreach (string blockedDomain in _filterDomains) { if (qname.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase) || qname.EndsWith("." + blockedDomain, StringComparison.OrdinalIgnoreCase)) { filterDomain = true; break; } } if (!filterDomain) return response; DnsDatagram aResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(qname, DnsResourceRecordType.A, DnsClass.IN), 2000); if (aResponse.RCODE != DnsResponseCode.NoError) return response; foreach (DnsResourceRecord record in aResponse.Answer) { if (record.Type == DnsResourceRecordType.A) { //domain has an A record; filter current AAAA response List answer = new List(); foreach (DnsResourceRecord record2 in response.Answer) { if (record2.Type == DnsResourceRecordType.CNAME) { answer.Add(record2); qname = (record2.RDATA as DnsCNAMERecordData).Domain; } } DnsResourceRecord[] authority = [new DnsResourceRecord(qname, DnsResourceRecordType.SOA, DnsClass.IN, _defaultTtl, new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 3600, 900, 86400, _defaultTtl))]; return new DnsDatagram(response.Identifier, true, response.OPCODE, false, false, response.RecursionDesired, response.RecursionAvailable, false, false, DnsResponseCode.NoError, response.Question, answer, authority); } } //domain does not have an A record; return current response return response; } #endregion #region properties public string Description { get { return "Filters AAAA records by returning NO DATA response when A records for the same domain name are available."; } } #endregion } } ================================================ FILE: Apps/FilterAaaaApp/FilterAaaaApp.csproj ================================================  net9.0 false 4.0 false Technitium Technitium DNS Server Shreyas Zare FilterAaaaApp FilterAaaa https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows filtering AAAA records by returning NO DATA response when A records for the same domain name are available. This allows clients with dual-stack (IPv4 and IPv6) Internet connection to prefer using IPv4 to connect to websites and use IPv6 only when a website has no IPv4 support. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/FilterAaaaApp/README.md ================================================ # Filter AAAA The `Filter AAAA` app allows filtering `AAAA` records by returning `NODATA` responses when `A` records for the same domain name are available. This allows clients with dual-stack (IPv4 and IPv6) internet connections to prefer using IPv4 to connect to websites and use IPv6 only when a website has no IPv4 support. The app is a _post processor_. That means, it modifies a response generated by the DNS server before it is sent to the client. ## Configuration As any post processor, this app is configured globally in the app settings. Its configuration file is a JSON document which looks like the following: ``` { "enableFilterAaaa": true, "defaultTtl": 30, "bypassLocalZones": false, "bypassNetworks": [ "192.168.1.0/24" ], "bypassDomains": [ "example.com" ], "filterDomains": [ ] } ``` The individual settings are: - `enableFilterAaaa`: when set to `false`, this app is disabled and passes through the original response. - `defaultTtl`: The default TTL (seconds) to use for the response. This will be used by clients to cache negative response. - `bypassLocalZones`: when set to `true`, authoritative answers are passed through unmodified. - `bypassNetworks`: a list of networks. If a request originates from a client in any of the specified networks, the original response is passed through unmodified. - `bypassDomains` a list of domain names. If a request is for a domain in this list, the original response is passed through unmodified. This includes subdomains of the domains in `bypassDomains`, i.e. `example.com` also matches `subdomain.example.com`. - `filterDomains` a list of domain names. If the list of filtered domain names is specified then the app will filter AAAA responses only for the specified domain names and their subdomain names. When the list is empty then the app will filter AAAA responses for all domain names. ## Post-processing The app processes any response which matches all of the following criteria: - the response has a `NoError` response code - the query type is `AAAA` - the response contains at least one `AAAA` record - the request / response pair is not excluded by any configuration setting - a lookup for an up `A` record for the same domain is successful and returns an address Note that this means that `NXDOMAIN`, `SERVFAIL`, and `NODATA` responses are left unmodified. The matching responses are replaced by one which includes all the `CNAME` records from the original response and a `SOA` record, but no `AAAA` record. ================================================ FILE: Apps/FilterAaaaApp/dnsApp.config ================================================ { "enableFilterAaaa": true, "defaultTtl": 30, "bypassLocalZones": false, "bypassNetworks": [ ], "bypassDomains": [ "example.com" ], "filterDomains": [ ] } ================================================ FILE: Apps/GeoContinentApp/Address.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2.Responses; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace GeoContinent { public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; MaxMind _maxMind; #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_maxMind is not null) _maxMind.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _maxMind = MaxMind.Create(dnsServer); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonContinent = default; byte scopePrefixLength = 0; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null)) scopePrefixLength = (byte)csIsp.Network.PrefixLength; else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null)) scopePrefixLength = (byte)csAsn.Network.PrefixLength; else scopePrefixLength = requestECS.SourcePrefixLength; if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) { if (!jsonAppRecordData.TryGetProperty(csResponse.Continent.Code, out jsonContinent)) jsonAppRecordData.TryGetProperty("default", out jsonContinent); } } if (jsonContinent.ValueKind == JsonValueKind.Undefined) { if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response)) { if (!jsonAppRecordData.TryGetProperty(response.Continent.Code, out jsonContinent)) jsonAppRecordData.TryGetProperty("default", out jsonContinent); } else { jsonAppRecordData.TryGetProperty("default", out jsonContinent); } if (jsonContinent.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); } List answers = new List(); switch (question.Type) { case DnsResourceRecordType.A: foreach (JsonElement jsonAddress in jsonContinent.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); } break; case DnsResourceRecordType.AAAA: foreach (JsonElement jsonAddress in jsonContinent.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); } break; } if (answers.Count == 0) return Task.FromResult(null); if (answers.Count > 1) answers.Shuffle(); EDnsOption[] options = null; if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } default: return Task.FromResult(null); } } #endregion #region properties public string Description { get { return "Returns A or AAAA records based on the continent the client queries from using MaxMind GeoIP2 Country database. Use the two character continent code like \"NA\" (North America) or \"OC\" (Oceania)."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""EU"": [ ""1.1.1.1"", ""2.2.2.2"" ], ""default"": [ ""3.3.3.3"" ] }"; } } #endregion } } ================================================ FILE: Apps/GeoContinentApp/CNAME.cs ================================================ /* 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.ApplicationCommon; using MaxMind.GeoIP2.Responses; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace GeoContinent { public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; MaxMind _maxMind; #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_maxMind is not null) _maxMind.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _maxMind = MaxMind.Create(dnsServer); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonContinent = default; string continentCode = null; byte scopePrefixLength = 0; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null)) scopePrefixLength = (byte)csIsp.Network.PrefixLength; else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null)) scopePrefixLength = (byte)csAsn.Network.PrefixLength; else scopePrefixLength = requestECS.SourcePrefixLength; if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) { string cc = csResponse.Continent.Code; if (!jsonAppRecordData.TryGetProperty(cc, out jsonContinent)) { jsonAppRecordData.TryGetProperty("default", out jsonContinent); continentCode = cc is null ? "default" : cc.ToLowerInvariant(); } } } if (jsonContinent.ValueKind == JsonValueKind.Undefined) { if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response)) { string cc = response.Continent.Code; if (!jsonAppRecordData.TryGetProperty(cc, out jsonContinent)) { jsonAppRecordData.TryGetProperty("default", out jsonContinent); continentCode = cc is null ? "default" : cc.ToLowerInvariant(); } } else { jsonAppRecordData.TryGetProperty("default", out jsonContinent); continentCode = "default"; } if (jsonContinent.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); } string cname = jsonContinent.GetString(); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); if (continentCode is not null) cname = cname.Replace("{ContinentCode}", continentCode, StringComparison.OrdinalIgnoreCase); IReadOnlyList answers; if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME else answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) }; EDnsOption[] options = null; if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } #endregion #region properties public string Description { get { return "Returns CNAME record based on the continent the client queries from using MaxMind GeoIP2 Country database. Note that the app will return ANAME record for an APP record at zone apex. Use the two character continent code like \"NA\" (North America) or \"OC\" (Oceania). You can also use '{ContinentCode}' variable in the default case domain name which will get replaced by the app using the client's actual continent code or 'default' if not found."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""EU"": ""eu.example.com"", ""default"": ""example.com"" }"; } } #endregion } } ================================================ FILE: Apps/GeoContinentApp/GeoContinentApp.csproj ================================================  net9.0 false true 9.0.1 false Technitium Technitium DNS Server Shreyas Zare GeoContinentApp GeoContinent https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record based on the continent the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest PreserveNewest ================================================ FILE: Apps/GeoContinentApp/MaxMind.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2; using System; using System.IO; namespace GeoContinent { class MaxMind : IDisposable { #region variables static MaxMind _maxMind; readonly DatabaseReader _mmCountryReader; readonly DatabaseReader _mmIspReader; readonly DatabaseReader _mmAsnReader; #endregion #region constructor private MaxMind(IDnsServer dnsServer) { string mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, "GeoIP2-Country.mmdb"); if (!File.Exists(mmCountryFile)) mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, "GeoLite2-Country.mmdb"); if (!File.Exists(mmCountryFile)) throw new FileNotFoundException("MaxMind Country file is missing!"); _mmCountryReader = new DatabaseReader(mmCountryFile); string mmIspFile = Path.Combine(dnsServer.ApplicationFolder, "GeoIP2-ISP.mmdb"); if (File.Exists(mmIspFile)) { _mmIspReader = new DatabaseReader(mmIspFile); return; } string mmAsnFile = Path.Combine(dnsServer.ApplicationFolder, "GeoLite2-ASN.mmdb"); if (File.Exists(mmAsnFile)) _mmAsnReader = new DatabaseReader(mmAsnFile); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _mmCountryReader?.Dispose(); _mmIspReader?.Dispose(); _mmAsnReader?.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region public public static MaxMind Create(IDnsServer dnsServer) { if (_maxMind is null) _maxMind = new MaxMind(dnsServer); return _maxMind; } #endregion #region properties public DatabaseReader CountryReader { get { return _mmCountryReader; } } public DatabaseReader IspReader { get { return _mmIspReader; } } public DatabaseReader AsnReader { get { return _mmAsnReader; } } #endregion } } ================================================ FILE: Apps/GeoContinentApp/ReadMe.txt ================================================ Using MaxMind GeoIP2 Database ============================= This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. For production usage, it is required that you purchase the GeoIP2 database from MaxMind (https://www.maxmind.com/) and use it. To update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method as above. ================================================ FILE: Apps/GeoContinentApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/GeoCountryApp/Address.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2.Responses; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace GeoCountry { public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; MaxMind _maxMind; #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_maxMind is not null) _maxMind.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _maxMind = MaxMind.Create(dnsServer); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonCountry = default; byte scopePrefixLength = 0; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null)) scopePrefixLength = (byte)csIsp.Network.PrefixLength; else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null)) scopePrefixLength = (byte)csAsn.Network.PrefixLength; else scopePrefixLength = requestECS.SourcePrefixLength; if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) { if (!jsonAppRecordData.TryGetProperty(csResponse.Country.IsoCode, out jsonCountry)) jsonAppRecordData.TryGetProperty("default", out jsonCountry); } } if (jsonCountry.ValueKind == JsonValueKind.Undefined) { if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response)) { if (!jsonAppRecordData.TryGetProperty(response.Country.IsoCode, out jsonCountry)) jsonAppRecordData.TryGetProperty("default", out jsonCountry); } else { jsonAppRecordData.TryGetProperty("default", out jsonCountry); } if (jsonCountry.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); } List answers = new List(); switch (question.Type) { case DnsResourceRecordType.A: foreach (JsonElement jsonAddress in jsonCountry.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); } break; case DnsResourceRecordType.AAAA: foreach (JsonElement jsonAddress in jsonCountry.EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); } break; } if (answers.Count == 0) return Task.FromResult(null); if (answers.Count > 1) answers.Shuffle(); EDnsOption[] options = null; if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } default: return Task.FromResult(null); } } #endregion #region properties public string Description { get { return "Returns A or AAAA records based on the country the client queries from using MaxMind GeoIP2 Country database. Use the two-character ISO 3166-1 alpha code for the country."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""IN"": [ ""1.1.1.1"", ""2.2.2.2"" ], ""default"": [ ""3.3.3.3"" ] }"; } } #endregion } } ================================================ FILE: Apps/GeoCountryApp/CNAME.cs ================================================ /* 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.ApplicationCommon; using MaxMind.GeoIP2.Responses; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace GeoCountry { public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; MaxMind _maxMind; #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_maxMind is not null) _maxMind.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _maxMind = MaxMind.Create(dnsServer); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonCountry = default; string countryCode = null; byte scopePrefixLength = 0; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null)) scopePrefixLength = (byte)csIsp.Network.PrefixLength; else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null)) scopePrefixLength = (byte)csAsn.Network.PrefixLength; else scopePrefixLength = requestECS.SourcePrefixLength; if (_maxMind.CountryReader.TryCountry(requestECS.Address, out CountryResponse csResponse)) { string cc = csResponse.Country.IsoCode; if (!jsonAppRecordData.TryGetProperty(cc, out jsonCountry)) { jsonAppRecordData.TryGetProperty("default", out jsonCountry); countryCode = cc is null ? "default" : cc.ToLowerInvariant(); } } } if (jsonCountry.ValueKind == JsonValueKind.Undefined) { if (_maxMind.CountryReader.TryCountry(remoteEP.Address, out CountryResponse response)) { string cc = response.Country.IsoCode; if (!jsonAppRecordData.TryGetProperty(cc, out jsonCountry)) { jsonAppRecordData.TryGetProperty("default", out jsonCountry); countryCode = cc is null ? "default" : cc.ToLowerInvariant(); } } else { jsonAppRecordData.TryGetProperty("default", out jsonCountry); countryCode = "default"; } if (jsonCountry.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); } string cname = jsonCountry.GetString(); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); if (countryCode is not null) cname = cname.Replace("{CountryCode}", countryCode, StringComparison.OrdinalIgnoreCase); IReadOnlyList answers; if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME else answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) }; EDnsOption[] options = null; if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } #endregion #region properties public string Description { get { return "Returns CNAME record based on the country the client queries from using MaxMind GeoIP2 Country database. Note that the app will return ANAME record for an APP record at zone apex. Use the two-character ISO 3166-1 alpha code for the country. You can also use '{CountryCode}' variable in the default case domain name which will get replaced by the app using the client's actual ISO country code or 'default' if not found."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""IN"": ""in.example.com"", ""default"": ""example.com"" }"; } } #endregion } } ================================================ FILE: Apps/GeoCountryApp/GeoCountryApp.csproj ================================================  net9.0 false true 9.0.1 false Technitium Technitium DNS Server Shreyas Zare GeoCountryApp GeoCountry https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record based on the country the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest PreserveNewest ================================================ FILE: Apps/GeoCountryApp/MaxMind.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2; using System; using System.IO; namespace GeoCountry { class MaxMind : IDisposable { #region variables static MaxMind _maxMind; readonly DatabaseReader _mmCountryReader; readonly DatabaseReader _mmIspReader; readonly DatabaseReader _mmAsnReader; #endregion #region constructor private MaxMind(IDnsServer dnsServer) { string mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, "GeoIP2-Country.mmdb"); if (!File.Exists(mmCountryFile)) mmCountryFile = Path.Combine(dnsServer.ApplicationFolder, "GeoLite2-Country.mmdb"); if (!File.Exists(mmCountryFile)) throw new FileNotFoundException("MaxMind Country file is missing!"); _mmCountryReader = new DatabaseReader(mmCountryFile); string mmIspFile = Path.Combine(dnsServer.ApplicationFolder, "GeoIP2-ISP.mmdb"); if (File.Exists(mmIspFile)) { _mmIspReader = new DatabaseReader(mmIspFile); return; } string mmAsnFile = Path.Combine(dnsServer.ApplicationFolder, "GeoLite2-ASN.mmdb"); if (File.Exists(mmAsnFile)) _mmAsnReader = new DatabaseReader(mmAsnFile); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _mmCountryReader?.Dispose(); _mmIspReader?.Dispose(); _mmAsnReader?.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region public public static MaxMind Create(IDnsServer dnsServer) { if (_maxMind is null) _maxMind = new MaxMind(dnsServer); return _maxMind; } #endregion #region properties public DatabaseReader CountryReader { get { return _mmCountryReader; } } public DatabaseReader IspReader { get { return _mmIspReader; } } public DatabaseReader AsnReader { get { return _mmAsnReader; } } #endregion } } ================================================ FILE: Apps/GeoCountryApp/ReadMe.txt ================================================ Using MaxMind GeoIP2 Database ============================= This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. For production usage, it is required that you purchase the GeoIP2 database from MaxMind (https://www.maxmind.com/) and use it. To update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method as above. ================================================ FILE: Apps/GeoCountryApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/GeoDistanceApp/Address.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2.Model; using MaxMind.GeoIP2.Responses; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace GeoDistance { public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; MaxMind _maxMind; #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_maxMind is not null) _maxMind.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region private private static double GetDistance(double lat1, double long1, double lat2, double long2) { double d1 = lat1 * (Math.PI / 180.0); double num1 = long1 * (Math.PI / 180.0); double d2 = lat2 * (Math.PI / 180.0); double num2 = long2 * (Math.PI / 180.0) - num1; double d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0); return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3))); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _maxMind = MaxMind.Create(dnsServer); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: Location location = null; byte scopePrefixLength = 0; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null)) scopePrefixLength = (byte)csIsp.Network.PrefixLength; else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null)) scopePrefixLength = (byte)csAsn.Network.PrefixLength; else scopePrefixLength = requestECS.SourcePrefixLength; if (_maxMind.CityReader.TryCity(requestECS.Address, out CityResponse csResponse) && csResponse.Location.HasCoordinates) location = csResponse.Location; } if ((location is null) && _maxMind.CityReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates) location = response.Location; using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonClosestServer = default; if (location is null) { if (jsonAppRecordData.GetArrayLength() > 0) jsonClosestServer = jsonAppRecordData[0]; } else { double lastDistance = double.MaxValue; foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray()) { double lat = Convert.ToDouble(jsonServer.GetProperty("lat").GetString()); double @long = Convert.ToDouble(jsonServer.GetProperty("long").GetString()); double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value); if (distance < lastDistance) { lastDistance = distance; jsonClosestServer = jsonServer; } } } if (jsonClosestServer.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); List answers = new List(); switch (question.Type) { case DnsResourceRecordType.A: foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty("addresses").EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetwork) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); } break; case DnsResourceRecordType.AAAA: foreach (JsonElement jsonAddress in jsonClosestServer.GetProperty("addresses").EnumerateArray()) { IPAddress address = IPAddress.Parse(jsonAddress.GetString()); if (address.AddressFamily == AddressFamily.InterNetworkV6) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); } break; } if (answers.Count == 0) return Task.FromResult(null); if (answers.Count > 1) answers.Shuffle(); EDnsOption[] options = null; if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } default: return Task.FromResult(null); } } #endregion #region properties public string Description { get { return "Returns A or AAAA records of the server located geographically closest to the client using MaxMind GeoIP2 City database. Use the geographic coordinates in decimal degrees (DD) form for the city the server is located in."; } } public string ApplicationRecordDataTemplate { get { return @"[ { ""name"": ""server1-mumbai"", ""lat"": ""19.07283"", ""long"": ""72.88261"", ""addresses"": [ ""1.1.1.1"" ] }, { ""name"": ""server2-london"", ""lat"": ""51.50853"", ""long"": ""-0.12574"", ""addresses"": [ ""2.2.2.2"" ] } ]"; } } #endregion } } ================================================ FILE: Apps/GeoDistanceApp/CNAME.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2.Model; using MaxMind.GeoIP2.Responses; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace GeoDistance { public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler { #region variables IDnsServer _dnsServer; MaxMind _maxMind; #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_maxMind is not null) _maxMind.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region private private static double GetDistance(double lat1, double long1, double lat2, double long2) { double d1 = lat1 * (Math.PI / 180.0); double num1 = long1 * (Math.PI / 180.0); double d2 = lat2 * (Math.PI / 180.0); double num2 = long2 * (Math.PI / 180.0) - num1; double d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) + Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0); return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3))); } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _maxMind = MaxMind.Create(dnsServer); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); Location location = null; byte scopePrefixLength = 0; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { if ((_maxMind.IspReader is not null) && _maxMind.IspReader.TryIsp(requestECS.Address, out IspResponse csIsp) && (csIsp.Network is not null)) scopePrefixLength = (byte)csIsp.Network.PrefixLength; else if ((_maxMind.AsnReader is not null) && _maxMind.AsnReader.TryAsn(requestECS.Address, out AsnResponse csAsn) && (csAsn.Network is not null)) scopePrefixLength = (byte)csAsn.Network.PrefixLength; else scopePrefixLength = requestECS.SourcePrefixLength; if (_maxMind.CityReader.TryCity(requestECS.Address, out CityResponse csResponse) && csResponse.Location.HasCoordinates) location = csResponse.Location; } if ((location is null) && _maxMind.CityReader.TryCity(remoteEP.Address, out CityResponse response) && response.Location.HasCoordinates) location = response.Location; using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonClosestServer = default; if (location is null) { if (jsonAppRecordData.GetArrayLength() > 0) jsonClosestServer = jsonAppRecordData[0]; } else { double lastDistance = double.MaxValue; foreach (JsonElement jsonServer in jsonAppRecordData.EnumerateArray()) { double lat = Convert.ToDouble(jsonServer.GetProperty("lat").GetString()); double @long = Convert.ToDouble(jsonServer.GetProperty("long").GetString()); double distance = GetDistance(lat, @long, location.Latitude.Value, location.Longitude.Value); if (distance < lastDistance) { lastDistance = distance; jsonClosestServer = jsonServer; } } } if (jsonClosestServer.ValueKind == JsonValueKind.Undefined) return Task.FromResult(null); string cname = jsonClosestServer.GetPropertyValue("cname", null); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); IReadOnlyList answers; if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME else answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) }; EDnsOption[] options = null; if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, scopePrefixLength, requestECS.Address); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers, null, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options)); } #endregion #region properties public string Description { get { return "Returns CNAME record of the server located geographically closest to the client using MaxMind GeoIP2 City database. Note that the app will return ANAME record for an APP record at zone apex. Use the geographic coordinates in decimal degrees (DD) form for the city the server is located in."; } } public string ApplicationRecordDataTemplate { get { return @"[ { ""name"": ""server1-mumbai"", ""lat"": ""19.07283"", ""long"": ""72.88261"", ""cname"": ""mumbai.example.com"" }, { ""name"": ""server2-london"", ""lat"": ""51.50853"", ""long"": ""-0.12574"", ""cname"": ""london.example.com"" } ]"; } } #endregion } } ================================================ FILE: Apps/GeoDistanceApp/GeoDistanceApp.csproj ================================================  net9.0 false true 9.0.1 false Technitium Technitium DNS Server Shreyas Zare GeoDistanceApp GeoDistance https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record of the server located geographically closest to the client using MaxMind GeoIP2 City database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-City.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest PreserveNewest ================================================ FILE: Apps/GeoDistanceApp/MaxMind.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using MaxMind.GeoIP2; using System; using System.IO; namespace GeoDistance { class MaxMind : IDisposable { #region variables static MaxMind _maxMind; readonly DatabaseReader _mmCityReader; readonly DatabaseReader _mmIspReader; readonly DatabaseReader _mmAsnReader; #endregion #region constructor private MaxMind(IDnsServer dnsServer) { string mmCityFile = Path.Combine(dnsServer.ApplicationFolder, "GeoIP2-City.mmdb"); if (!File.Exists(mmCityFile)) mmCityFile = Path.Combine(dnsServer.ApplicationFolder, "GeoLite2-City.mmdb"); if (!File.Exists(mmCityFile)) throw new FileNotFoundException("MaxMind City file is missing!"); _mmCityReader = new DatabaseReader(mmCityFile); string mmIspFile = Path.Combine(dnsServer.ApplicationFolder, "GeoIP2-ISP.mmdb"); if (File.Exists(mmIspFile)) { _mmIspReader = new DatabaseReader(mmIspFile); return; } string mmAsnFile = Path.Combine(dnsServer.ApplicationFolder, "GeoLite2-ASN.mmdb"); if (File.Exists(mmAsnFile)) _mmAsnReader = new DatabaseReader(mmAsnFile); } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _mmCityReader?.Dispose(); _mmIspReader?.Dispose(); _mmAsnReader?.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region public public static MaxMind Create(IDnsServer dnsServer) { if (_maxMind is null) _maxMind = new MaxMind(dnsServer); return _maxMind; } #endregion #region properties public DatabaseReader CityReader { get { return _mmCityReader; } } public DatabaseReader IspReader { get { return _mmIspReader; } } public DatabaseReader AsnReader { get { return _mmAsnReader; } } #endregion } } ================================================ FILE: Apps/GeoDistanceApp/ReadMe.txt ================================================ Using MaxMind GeoIP2 Database ============================= WARNING: Latitude and longitude are not precise and should not be used to identify a particular street address or household. This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. For production usage, it is required that you purchase the GeoIP2 database from MaxMind (https://www.maxmind.com/) and use it. To update the MaxMind GeoIP2 database for your app, download the GeoIP2-City.mmdb file and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method as above. ================================================ FILE: Apps/GeoDistanceApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/LogExporterApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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.ApplicationCommon; using LogExporter.Strategy; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; namespace LogExporter { public sealed class App : IDnsApplication, IDnsQueryLogger { #region variables IDnsServer? _dnsServer; AppConfig? _config; readonly ExportManager _exportManager = new ExportManager(); bool _enableLogging; readonly ConcurrentQueue _queuedLogs = new ConcurrentQueue(); readonly Timer _queueTimer; const int QUEUE_TIMER_INTERVAL = 10000; const int BULK_INSERT_COUNT = 1000; bool _disposed; #endregion #region constructor public App() { _queueTimer = new Timer(HandleExportLogCallback); } #endregion #region IDisposable public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _queueTimer?.Dispose(); ExportLogsAsync().Sync(); //flush any pending logs _exportManager.Dispose(); } _disposed = true; } } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _config = AppConfig.Deserialize(config); if (_config is null) throw new DnsClientException("Invalid application configuration."); if (_config.FileTarget!.Enabled) { _exportManager.RemoveStrategy(typeof(FileExportStrategy)); _exportManager.AddStrategy(new FileExportStrategy(_config.FileTarget!.Path)); } else { _exportManager.RemoveStrategy(typeof(FileExportStrategy)); } if (_config.HttpTarget!.Enabled) { _exportManager.RemoveStrategy(typeof(HttpExportStrategy)); _exportManager.AddStrategy(new HttpExportStrategy(_config.HttpTarget.Endpoint, _config.HttpTarget.Headers)); } else { _exportManager.RemoveStrategy(typeof(HttpExportStrategy)); } if (_config.SyslogTarget!.Enabled) { _exportManager.RemoveStrategy(typeof(SyslogExportStrategy)); _exportManager.AddStrategy(new SyslogExportStrategy(_config.SyslogTarget.Address, _config.SyslogTarget.Port, _config.SyslogTarget.Protocol)); } else { _exportManager.RemoveStrategy(typeof(SyslogExportStrategy)); } _enableLogging = _exportManager.HasStrategy(); if (_enableLogging) _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite); else _queueTimer.Change(Timeout.Infinite, Timeout.Infinite); return Task.CompletedTask; } public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (_enableLogging) { if (_queuedLogs.Count < _config!.MaxQueueSize) _queuedLogs.Enqueue(new LogEntry(timestamp, remoteEP, protocol, request, response, _config.EnableEdnsLogging)); } return Task.CompletedTask; } #endregion #region private private async Task ExportLogsAsync() { try { List logs = new List(BULK_INSERT_COUNT); while (true) { while (logs.Count < BULK_INSERT_COUNT && _queuedLogs.TryDequeue(out LogEntry? log)) { logs.Add(log); } if (logs.Count < 1) break; await _exportManager.ImplementStrategyAsync(logs); logs.Clear(); } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } } private async void HandleExportLogCallback(object? state) { try { // Process logs within the timer interval, then let the timer reschedule await ExportLogsAsync(); } catch (Exception ex) { _dnsServer?.WriteLog(ex); } finally { try { _queueTimer?.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } } #endregion #region properties public string Description { get { return "Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols)."; } } #endregion } } ================================================ FILE: Apps/LogExporterApp/AppConfig.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; namespace LogExporter { public class AppConfig { [JsonPropertyName("maxQueueSize")] public int MaxQueueSize { get; set; } [JsonPropertyName("enableEdnsLogging ")] public bool EnableEdnsLogging { get; set; } [JsonPropertyName("file")] public FileTarget? FileTarget { get; set; } [JsonPropertyName("http")] public HttpTarget? HttpTarget { get; set; } [JsonPropertyName("syslog")] public SyslogTarget? SyslogTarget { get; set; } // Load configuration from JSON public static AppConfig? Deserialize(string json) { return JsonSerializer.Deserialize(json, DnsConfigSerializerOptions.Default); } } public class TargetBase { [JsonPropertyName("enabled")] public bool Enabled { get; set; } } public class SyslogTarget : TargetBase { [JsonPropertyName("address")] public required string Address { get; set; } [JsonPropertyName("port")] public int? Port { get; set; } [JsonPropertyName("protocol")] public string? Protocol { get; set; } } public class FileTarget : TargetBase { [JsonPropertyName("path")] public required string Path { get; set; } } public class HttpTarget : TargetBase { [JsonPropertyName("endpoint")] public required string Endpoint { get; set; } [JsonPropertyName("headers")] public Dictionary? Headers { get; set; } } // Setup reusable options with a single instance public static class DnsConfigSerializerOptions { public static readonly JsonSerializerOptions Default = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding NumberHandling = JsonNumberHandling.Strict, AllowTrailingCommas = true, // Allow trailing commas in JSON DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Convert dictionary keys to camelCase DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values }; } } ================================================ FILE: Apps/LogExporterApp/LogEntry.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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.ApplicationCommon; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.Json; using System.Text.Json.Serialization; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace LogExporter { public class LogEntry { public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response, bool ednsLogging = false) { // Assign timestamp and ensure it's in UTC Timestamp = timestamp.Kind == DateTimeKind.Utc ? timestamp : timestamp.ToUniversalTime(); // Extract client information ClientIp = remoteEP.Address.ToString(); Protocol = protocol; ResponseType = response.Tag == null ? DnsServerResponseType.Recursive : (DnsServerResponseType)response.Tag; if ((ResponseType == DnsServerResponseType.Recursive) && (response.Metadata is not null)) ResponseRtt = response.Metadata.RoundTripTime; ResponseCode = response.RCODE; // Extract request information if (request.Question.Count > 0) { DnsQuestionRecord query = request.Question[0]; Question = new DnsQuestion { QuestionName = query.Name, QuestionType = query.Type, QuestionClass = query.Class, }; } // Convert answer section into a simple string summary (comma-separated for multiple answers) Answers = new List(response.Answer.Count); if (response.Answer.Count > 0) { Answers.AddRange(response.Answer.Select(record => new DnsResourceRecord { Name = record.Name, RecordType = record.Type, RecordClass = record.Class, RecordTtl = record.TTL, RecordData = record.RDATA.ToString(), DnssecStatus = record.DnssecStatus, })); } EDNS = new List(); if (!ednsLogging || response.EDNS is null) { return; } foreach (EDnsOption extendedErrorLog in response.EDNS.Options.Where(o => o.Code == EDnsOptionCode.EXTENDED_DNS_ERROR)) { string[] extractedData = extendedErrorLog.Data.ToString().Replace("[", string.Empty).Replace("]", string.Empty).Split(":", StringSplitOptions.TrimEntries); EDNS.Add(new EDNSLog { ErrType = extractedData[0], Message = extractedData[1] }); } } public List Answers { get; private set; } public string ClientIp { get; private set; } public List EDNS { get; private set; } public DnsTransportProtocol Protocol { get; private set; } public DnsQuestion? Question { get; private set; } public DnsResponseCode ResponseCode { get; private set; } public double? ResponseRtt { get; private set; } public DnsServerResponseType ResponseType { get; private set; } public DateTime Timestamp { get; private set; } public override string ToString() { return JsonSerializer.Serialize(this, DnsLogSerializerOptions.Default); } public static class DnsLogSerializerOptions { public static readonly JsonSerializerOptions Default = new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() }, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.Strict, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; } public class DnsQuestion { public DnsClass QuestionClass { get; set; } public required string QuestionName { get; set; } public DnsResourceRecordType QuestionType { get; set; } } public class DnsResourceRecord { public DnssecStatus DnssecStatus { get; set; } public required string Name { get; set; } public DnsClass RecordClass { get; set; } public required string RecordData { get; set; } public uint RecordTtl { get; set; } public DnsResourceRecordType RecordType { get; set; } } public class EDNSLog { public string? ErrType { get; set; } public string? Message { get; set; } } public class JsonDateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string? dts = reader.GetString(); return dts == null ? DateTime.MinValue : DateTime.Parse(dts); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); } } } } ================================================ FILE: Apps/LogExporterApp/LogExporterApp.csproj ================================================  net9.0 false true 2.1 false Technitium Technitium DNS Server Zafer Balkan LogExporterApp LogExporter https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols). false Library enable false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false PreserveNewest ================================================ FILE: Apps/LogExporterApp/Strategy/ExportManager.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace LogExporter.Strategy { public sealed class ExportManager : IDisposable { #region variables readonly ConcurrentDictionary _exportStrategies = new ConcurrentDictionary(); #endregion #region IDisposable public void Dispose() { foreach (KeyValuePair exportStrategy in _exportStrategies) exportStrategy.Value.Dispose(); } #endregion #region public public void AddStrategy(IExportStrategy strategy) { if (!_exportStrategies.TryAdd(strategy.GetType(), strategy)) throw new InvalidOperationException(); } public void RemoveStrategy(Type type) { if (_exportStrategies.TryRemove(type, out IExportStrategy? existing)) existing?.Dispose(); } public bool HasStrategy() { return !_exportStrategies.IsEmpty; } public async Task ImplementStrategyAsync(IReadOnlyList logs) { List tasks = new List(_exportStrategies.Count); foreach (KeyValuePair strategy in _exportStrategies) { tasks.Add(Task.Factory.StartNew(delegate (object? state) { return strategy.Value.ExportAsync(logs); }, null, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current)); } await Task.WhenAll(tasks); } #endregion } } ================================================ FILE: Apps/LogExporterApp/Strategy/FileExportStrategy.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 Serilog; using System.Collections.Generic; using System.Threading.Tasks; namespace LogExporter.Strategy { public sealed class FileExportStrategy : IExportStrategy { #region variables readonly Serilog.Core.Logger _sender; bool _disposed; #endregion #region constructor public FileExportStrategy(string filePath) { _sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: "{Message:lj}{NewLine}{Exception}").CreateLogger(); } #endregion #region IDisposable public void Dispose() { if (!_disposed) { _sender.Dispose(); _disposed = true; } } #endregion #region public public Task ExportAsync(IReadOnlyList logs) { foreach (LogEntry logEntry in logs) _sender.Information(logEntry.ToString()); return Task.CompletedTask; } #endregion } } ================================================ FILE: Apps/LogExporterApp/Strategy/HttpExportStrategy.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 Microsoft.Extensions.Configuration; using Serilog; using Serilog.Sinks.Http; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace LogExporter.Strategy { public sealed class HttpExportStrategy : IExportStrategy { #region variables readonly Serilog.Core.Logger _sender; bool _disposed; #endregion #region constructor public HttpExportStrategy(string endpoint, Dictionary? headers = null) { IConfigurationRoot? configuration = null; if (headers != null) { configuration = new ConfigurationBuilder() .AddInMemoryCollection(headers) .Build(); } _sender = new LoggerConfiguration().WriteTo.Http(endpoint, null, httpClient: new CustomHttpClient(), configuration: configuration).Enrich.FromLogContext().CreateLogger(); } #endregion #region IDisposable public void Dispose() { if (!_disposed) { _sender.Dispose(); _disposed = true; } } #endregion #region public public Task ExportAsync(IReadOnlyList logs) { foreach (LogEntry logEntry in logs) _sender.Information(logEntry.ToString()); return Task.CompletedTask; } #endregion public class CustomHttpClient : IHttpClient { readonly HttpClient _httpClient; public CustomHttpClient() { _httpClient = new HttpClient(); } public void Configure(IConfiguration configuration) { foreach (IConfigurationSection pair in configuration.GetChildren()) { if (!_httpClient.DefaultRequestHeaders.TryAddWithoutValidation(pair.Key, pair.Value)) throw new FormatException($"Failed to add header '{pair.Key}'."); } } public void Dispose() { _httpClient?.Dispose(); GC.SuppressFinalize(this); } public async Task PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken) { StreamContent content = new StreamContent(contentStream); content.Headers.Add("Content-Type", "application/json"); return await _httpClient .PostAsync(requestUri, content, cancellationToken) .ConfigureAwait(false); } } } } ================================================ FILE: Apps/LogExporterApp/Strategy/IExportStrategy.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 System; using System.Collections.Generic; using System.Threading.Tasks; namespace LogExporter.Strategy { /// /// Strategy interface to decide the sinks for exporting the logs. /// public interface IExportStrategy: IDisposable { Task ExportAsync(IReadOnlyList logs); } } ================================================ FILE: Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 Serilog; using Serilog.Events; using Serilog.Parsing; using Serilog.Sinks.Syslog; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace LogExporter.Strategy { public sealed class SyslogExportStrategy : IExportStrategy { #region variables const string _appName = "Technitium DNS Server"; const string _sdId = "meta"; const string DEFAUL_PROTOCOL = "udp"; const int DEFAULT_PORT = 514; readonly Facility _facility = Facility.Local6; readonly Rfc5424Formatter _formatter; readonly Serilog.Core.Logger _sender; bool _disposed; #endregion #region constructor public SyslogExportStrategy(string address, int? port, string? protocol) { port ??= DEFAULT_PORT; protocol ??= DEFAUL_PROTOCOL; LoggerConfiguration conf = new LoggerConfiguration(); _sender = protocol.ToLowerInvariant() switch { "tls" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: true).Enrich.FromLogContext().CreateLogger(), "tcp" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: false).Enrich.FromLogContext().CreateLogger(), "udp" => conf.WriteTo.UdpSyslog(address, port.Value, _appName, SyslogFormat.RFC5424, _facility).Enrich.FromLogContext().CreateLogger(), "local" => conf.WriteTo.LocalSyslog(_appName, _facility).Enrich.FromLogContext().CreateLogger(), _ => throw new NotSupportedException("Syslog protocol is not supported: " + protocol), }; _formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName); } #endregion #region IDisposable public void Dispose() { if (!_disposed) { _sender.Dispose(); _disposed = true; } } #endregion #region public public Task ExportAsync(IReadOnlyList logs) { foreach (LogEntry log in logs) _sender.Information(_formatter.FormatMessage((LogEvent?)Convert(log))); return Task.CompletedTask; } #endregion #region private private static LogEvent Convert(LogEntry log) { // Initialize properties with base log details List properties = new List { new LogEventProperty("timestamp", new ScalarValue(log.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))), new LogEventProperty("clientIp", new ScalarValue(log.ClientIp)), new LogEventProperty("protocol", new ScalarValue(log.Protocol.ToString())), new LogEventProperty("responseType", new ScalarValue(log.ResponseType.ToString())), new LogEventProperty("responseRtt", new ScalarValue(log.ResponseRtt?.ToString())), new LogEventProperty("rCode", new ScalarValue(log.ResponseCode.ToString())) }; // Add each question as properties if (log.Question != null) { LogEntry.DnsQuestion question = log.Question; properties.Add(new LogEventProperty("qName", new ScalarValue(question.QuestionName))); properties.Add(new LogEventProperty("qType", new ScalarValue(question.QuestionType.ToString()))); properties.Add(new LogEventProperty("qClass", new ScalarValue(question.QuestionClass.ToString()))); string questionSummary = $"QNAME: {question.QuestionName}, QTYPE: {question.QuestionType}, QCLASS: {question.QuestionClass}"; properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(questionSummary))); } else { properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(string.Empty))); } // Add each answer as properties if (log.Answers.Count > 0) { for (int i = 0; i < log.Answers.Count; i++) { LogEntry.DnsResourceRecord answer = log.Answers[i]; properties.Add(new LogEventProperty($"aName_{i}", new ScalarValue(answer.Name))); properties.Add(new LogEventProperty($"aType_{i}", new ScalarValue(answer.RecordType.ToString()))); properties.Add(new LogEventProperty($"aClass_{i}", new ScalarValue(answer.RecordClass.ToString()))); properties.Add(new LogEventProperty($"aTtl_{i}", new ScalarValue(answer.RecordTtl.ToString()))); properties.Add(new LogEventProperty($"aRData_{i}", new ScalarValue(answer.RecordData))); properties.Add(new LogEventProperty($"aDnssecStatus_{i}", new ScalarValue(answer.DnssecStatus.ToString()))); } // Generate answers summary string answerSummary = string.Join(", ", log.Answers.Select(a => a.RecordData)); properties.Add(new LogEventProperty("answersSummary", new ScalarValue(answerSummary))); } else { properties.Add(new LogEventProperty("answersSummary", new ScalarValue(string.Empty))); } // Add EDNS logs if (log.EDNS.Count > 0) { for (int i = 0; i < log.EDNS.Count; i++) { var ednsLog = log.EDNS[i]; properties.Add(new LogEventProperty($"ednsErrType_{i}", new ScalarValue(ednsLog.ErrType))); properties.Add(new LogEventProperty($"ednsMessage_{i}", new ScalarValue(ednsLog.Message))); } } // Define the message template to match the original summary format const string templateText = "{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]"; // Parse the template MessageTemplate template = new MessageTemplateParser().Parse(templateText); // Create the LogEvent and return it return new LogEvent( timestamp: log.Timestamp, level: LogEventLevel.Information, exception: null, messageTemplate: template, properties: properties ); } #endregion } } ================================================ FILE: Apps/LogExporterApp/dnsApp.config ================================================ { "maxQueueSize": 1000000, "ebableEdnsLogging": false, "file": { "path": "./dns_logs.json", "enabled": false }, "http": { "endpoint": "http://localhost:5000/logs", "headers": { "Authorization": "Bearer abc123" }, "enabled": false }, "syslog": { "address": "127.0.0.1", "port": 514, "protocol": "UDP", "enabled": false } } ================================================ FILE: Apps/MispConnectorApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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.ApplicationCommon; using System; using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Http.Client; namespace MispConnector { public sealed class App : IDnsApplication, IDnsRequestBlockingHandler { #region variables string _domainCacheFilePath; Config _config; IDnsServer _dnsServer; FrozenSet _domainBlocklist = FrozenSet.Empty; HttpClient _httpClient; Uri _mispApiUrl; DnsSOARecordData _soaRecord; TimeSpan _updateInterval; Task _updateLoopTask; CancellationTokenSource _appShutdownCts; #endregion variables #region IDisposable public void Dispose() { _appShutdownCts?.Cancel(); try { if (_updateLoopTask != null) { _ = Task.WhenAny(_updateLoopTask, Task.Delay(TimeSpan.FromSeconds(2))).GetAwaiter().GetResult(); } } catch { } finally { _appShutdownCts?.Dispose(); _httpClient?.Dispose(); } } #endregion IDisposable #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; try { _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, 60); JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _config = JsonSerializer.Deserialize(config, options); Validator.ValidateObject(_config, new ValidationContext(_config), validateAllProperties: true); string configDir = _dnsServer.ApplicationFolder; Directory.CreateDirectory(configDir); _domainCacheFilePath = Path.Combine(configDir, "misp_domain_cache.txt"); _updateInterval = ParseUpdateInterval(_config.UpdateInterval); Uri mispServerUrl = new Uri(_config.MispServerUrl); _mispApiUrl = new Uri(mispServerUrl, "/attributes/restSearch"); _httpClient = CreateHttpClient(mispServerUrl, _config.DisableTlsValidation); await LoadBlocklistFromCacheAsync(); _appShutdownCts = new CancellationTokenSource(); // We do not await this, as it's designed to run for the lifetime of the app. _updateLoopTask = StartUpdateLoopAsync(_appShutdownCts.Token); Task _ = _updateLoopTask.ContinueWith(t => { if (t.IsFaulted) { _dnsServer.WriteLog($"FATAL: Update loop terminated unexpectedly: {t.Exception?.GetBaseException().Message}"); _dnsServer.WriteLog(t.Exception); } }, TaskContinuationOptions.OnlyOnFaulted); } catch (Exception ex) { _dnsServer.WriteLog($"FATAL: MISP Connector failed to initialize. Check configuration. Error: {ex.Message}"); _dnsServer.WriteLog(ex); } } public Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP) { return Task.FromResult(false); } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP) { if (_config?.EnableBlocking != true) { return Task.FromResult(null); } DnsQuestionRecord question = request.Question[0]; bool domainBlocked = IsDomainBlocked(question.Name, out string blockedDomain); if (!domainBlocked) { return Task.FromResult(null); } string blockingReport = $"source=misp-connector;domain={blockedDomain}"; EDnsOption[] options = null; if (_config.AddExtendedDnsError && request.EDNS is not null) { options = new EDnsOption[] { new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, string.Empty)) }; } if (_config.AllowTxtBlockingReport && question.Type == DnsResourceRecordType.TXT) { DnsResourceRecord[] answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData(string.Empty)) }; return Task.FromResult(new DnsDatagram( ID: request.Identifier, isResponse: true, OPCODE: DnsOpcode.StandardQuery, authoritativeAnswer: false, truncation: false, recursionDesired: request.RecursionDesired, recursionAvailable: true, authenticData: false, checkingDisabled: false, RCODE: DnsResponseCode.NoError, question: request.Question, answer: answer, authority: null, additional: null, udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, ednsFlags: EDnsHeaderFlags.None, options: options )); } DnsResourceRecord[] authority = { new DnsResourceRecord(question.Name, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord) }; return Task.FromResult(new DnsDatagram( ID: request.Identifier, isResponse: true, OPCODE: DnsOpcode.StandardQuery, authoritativeAnswer: true, truncation: false, recursionDesired: request.RecursionDesired, recursionAvailable: true, authenticData: false, checkingDisabled: false, RCODE: DnsResponseCode.NxDomain, question: request.Question, answer: null, authority: authority, additional: null, udpPayloadSize: request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, ednsFlags: EDnsHeaderFlags.None, options: options )); } #endregion public #region private private async Task StartUpdateLoopAsync(CancellationToken cancellationToken) { await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(5, 30)), cancellationToken); using (PeriodicTimer timer = new PeriodicTimer(_updateInterval)) { while (!cancellationToken.IsCancellationRequested) { try { await UpdateIocsAsync(cancellationToken); } catch (OperationCanceledException) { _dnsServer.WriteLog("Update loop is shutting down gracefully."); break; } catch (Exception ex) { _dnsServer.WriteLog($"FATAL: The MispConnector update task failed unexpectedly. Error: {ex.Message}"); _dnsServer.WriteLog(ex); } await timer.WaitForNextTickAsync(cancellationToken); } } } private static TimeSpan ParseUpdateInterval(string interval) { if (string.IsNullOrWhiteSpace(interval) || interval.Length < 2) { throw new FormatException("Update interval is not in a valid format (e.g., '60m', '2h', '7d')."); } string unit = interval.Substring(interval.Length - 1).ToLowerInvariant(); string valueString = interval.Substring(0, interval.Length - 1); if (!int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) || value <= 0) { throw new FormatException($"Invalid numeric value '{valueString}' in update interval."); } switch (unit) { case "m": return TimeSpan.FromMinutes(value); case "h": return TimeSpan.FromHours(value); case "d": return TimeSpan.FromDays(value); default: throw new FormatException($"Invalid unit '{unit}' in update interval. Allowed units are 'm', 'h', 'd'."); } } private async Task CheckTcpPortAsync(Uri serverUri, CancellationToken cancellationToken) { string host = serverUri.DnsSafeHost; int port = serverUri.Port; TimeSpan timeout = TimeSpan.FromSeconds(5); _dnsServer.WriteLog($"Performing pre-flight TCP check for {host}:{port} with a {timeout.TotalSeconds}-second timeout..."); try { using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(timeout).Token)) using (TcpClient client = new TcpClient()) { await client.ConnectAsync(host, port, cts.Token); } _dnsServer.WriteLog($"Pre-flight TCP check successful for {host}:{port}."); return true; } catch (OperationCanceledException) { _dnsServer.WriteLog($"ERROR: Pre-flight TCP check failed: Connection to {host}:{port} timed out after {timeout.TotalSeconds} seconds. Check firewall rules or network route."); return false; } catch (SocketException ex) { _dnsServer.WriteLog($"ERROR: Pre-flight TCP check failed: A network error occurred for {host}:{port}. Error: {ex.Message}"); return false; } catch (Exception ex) { _dnsServer.WriteLog($"ERROR: An unexpected error occurred during the pre-flight TCP check for {host}:{port}. Error: {ex.Message}"); return false; } } private HttpClient CreateHttpClient(Uri serverUrl, bool disableTlsValidation) { HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsServer.Proxy; handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsServer; if (disableTlsValidation) { handler.InnerHandler.SslOptions.RemoteCertificateValidationCallback = delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; }; _dnsServer.WriteLog($"WARNING: TLS certificate validation is DISABLED for MISP server: {serverUrl}"); } return new HttpClient(handler); } private async Task> FetchIocFromMispAsync(CancellationToken cancellationToken) { HashSet iocSet = new HashSet(StringComparer.OrdinalIgnoreCase); int page = 1; int limit = _config.PaginationLimit; bool hasMorePages = true; _dnsServer.WriteLog($"Starting paginated fetch from MISP API with a page size of {limit}..."); const int maxRetries = 3; while (hasMorePages) { int attempt = 0; MispResponse mispResponse = null; while (attempt < maxRetries) { attempt++; try { MispRequestBody requestBody = new MispRequestBody { Type = "domain", To_ids = true, Deleted = false, Last = _config.MaxIocAge, Limit = limit, Page = page }; StringContent requestContent = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _mispApiUrl) { Content = requestContent }; request.Headers.Add("Authorization", _config.MispApiKey); request.Headers.Add("Accept", "application/json"); _dnsServer.WriteLog($"Fetching page {page}, attempt {attempt}/{maxRetries}..."); using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { // This is a definitive failure from the server (e.g., 403, 500). // We should not retry this. Abort immediately. string errorBody = await response.Content.ReadAsStringAsync(cancellationToken); throw new HttpRequestException($"MISP API returned a non-success status code: {(int)response.StatusCode}. Body: {errorBody}", null, response.StatusCode); } await using (Stream responseStream = await response.Content.ReadAsStreamAsync(cancellationToken)) mispResponse = await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken); break; } catch (Exception ex) when (ex is HttpRequestException || ex is SocketException || ex is OperationCanceledException) { // These are likely transient network errors, so we should retry. _dnsServer.WriteLog($"WARNING: A transient network error occurred on page {page}, attempt {attempt}/{maxRetries}. Error: {ex.Message}"); if (attempt < maxRetries) { TimeSpan delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); _dnsServer.WriteLog($"Waiting for {delay.TotalSeconds:F1} seconds before retrying..."); await Task.Delay(delay, cancellationToken); } else { // All retries have failed for this page. _dnsServer.WriteLog($"ERROR: Failed to fetch page {page} after {maxRetries} attempts. Aborting entire update cycle."); throw; } } } List attributes = mispResponse?.Response?.Attribute; if (attributes == null || attributes.Count == 0) { hasMorePages = false; continue; } foreach (MispAttribute attribute in attributes) { string ioc = attribute.Value?.Trim().ToLowerInvariant(); if (!string.IsNullOrEmpty(ioc)) { if (DnsClient.IsDomainNameValid(ioc)) { iocSet.Add(ioc); } } } // Assumption: If we received fewer items than our limit, it must be the last page. if (attributes.Count < limit) { hasMorePages = false; } else { page++; } } _dnsServer.WriteLog($"Finished paginated fetch. Freezing {iocSet.Count} IOCs for optimal read performance..."); return iocSet; } private bool IsDomainBlocked(string domain, out string foundZone) { FrozenSet currentBlocklist = _domainBlocklist; ReadOnlySpan currentSpan = domain.AsSpan(); while (true) { // To look up in a HashSet, we must provide a string. string key = new string(currentSpan); if (currentBlocklist.TryGetValue(key, out foundZone)) { return true; } int dotIndex = currentSpan.IndexOf('.'); if (dotIndex == -1) { break; // No more parent domains. } // Slice to the parent domain view. No allocation here. currentSpan = currentSpan.Slice(dotIndex + 1); } foundZone = null; return false; } private async Task LoadBlocklistFromCacheAsync() { if (File.Exists(_domainCacheFilePath)) { try { FrozenSet domains = (await File.ReadAllLinesAsync(_domainCacheFilePath)).ToHashSet(StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase); Interlocked.Exchange(ref _domainBlocklist, domains); _dnsServer.WriteLog($"MISP Connector: Loaded {domains.Count} domains from cache."); } catch (IOException ex) { _dnsServer.WriteLog($"ERROR: Failed to read cache file '{_domainCacheFilePath}'. Error: {ex.Message}"); } } } private async Task UpdateIocsAsync(CancellationToken cancellationToken) { if (!await CheckTcpPortAsync(new Uri(_config.MispServerUrl), cancellationToken)) { return; } _dnsServer.WriteLog("MISP Connector: Starting IOC update..."); HashSet tmpDomains = await FetchIocFromMispAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); FrozenSet domains = tmpDomains.ToFrozenSet(StringComparer.OrdinalIgnoreCase); if (!domains.SetEquals(_domainBlocklist)) { await WriteIocsToCacheAsync(domains, cancellationToken); Interlocked.Exchange(ref _domainBlocklist, domains); _dnsServer.WriteLog($"MISP Connector: Successfully updated blocklist with {domains.Count} domains."); } else { _dnsServer.WriteLog("MISP data has not changed. No update to blocklist or cache is necessary."); } } private async Task WriteIocsToCacheAsync(FrozenSet iocs, CancellationToken cancellationToken) { string tempPath = _domainCacheFilePath + ".tmp"; await File.WriteAllLinesAsync(tempPath, iocs, cancellationToken); File.Move(tempPath, _domainCacheFilePath, true); } #endregion private #region properties public string Description { get { return "A focused connector that imports domain IOCs from a MISP server to block malicious domains using direct REST API calls."; } } #endregion properties private class Config { [JsonPropertyName("addExtendedDnsError")] public bool AddExtendedDnsError { get; set; } = true; [JsonPropertyName("allowTxtBlockingReport")] public bool AllowTxtBlockingReport { get; set; } = true; [JsonPropertyName("disableTlsValidation")] public bool DisableTlsValidation { get; set; } = false; [JsonPropertyName("enableBlocking")] public bool EnableBlocking { get; set; } = true; [JsonPropertyName("maxIocAge")] [Required(ErrorMessage = "maxIocAge is a required configuration property.")] [RegularExpression(@"^\d+[mhd]$", ErrorMessage = "Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').", MatchTimeoutInMilliseconds = 3000)] public string MaxIocAge { get; set; } [JsonPropertyName("mispApiKey")] [Required(ErrorMessage = "mispApiKey is a required configuration property.")] [MinLength(1, ErrorMessage = "mispApiKey cannot be empty.")] public string MispApiKey { get; set; } [JsonPropertyName("mispServerUrl")] [Required(ErrorMessage = "mispServerUrl is a required configuration property.")] [Url(ErrorMessage = "mispServerUrl must be a valid URL.")] public string MispServerUrl { get; set; } [JsonPropertyName("paginationLimit")] public int PaginationLimit { get; set; } = 5000; [JsonPropertyName("updateInterval")] [Required(ErrorMessage = "updateInterval is a required configuration property.")] [RegularExpression(@"^\d+[mhd]$", ErrorMessage = "Invalid interval format. Use a number followed by 'm', 'h', or 'd' (e.g., '90m', '2h', '7d').", MatchTimeoutInMilliseconds = 3000)] public string UpdateInterval { get; set; } } private class MispAttribute { [JsonPropertyName("value")] public string Value { get; set; } } private class MispRequestBody { [JsonPropertyName("deleted")] public bool Deleted { get; set; } [JsonPropertyName("last")] public string Last { get; set; } [JsonPropertyName("limit")] public int Limit { get; set; } [JsonPropertyName("page")] public int Page { get; set; } [JsonPropertyName("to_ids")] public bool To_ids { get; set; } [JsonPropertyName("type")] public string Type { get; set; } } private class MispResponse { [JsonPropertyName("response")] public MispResponseData Response { get; set; } } private class MispResponseData { [JsonPropertyName("Attribute")] public List Attribute { get; set; } } } } ================================================ FILE: Apps/MispConnectorApp/MispConnectorApp.csproj ================================================  net9.0 false 1.0 false Technitium Technitium DNS Server Zafer Balkan MispConnectorApp MispConnector https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Block malicious domain names pulled from MISP feeds.\n\nNote! This app works independent of the DNS server's built-in blocking feature. The options configured in DNS server Settings section does not apply to this app. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/MispConnectorApp/README.md ================================================ # MISP Connector for Technitium DNS Server A plugin that pulls malicious domain names from MISP feeds and enforces blocking in Technitium DNS. It maintains in-memory blocklists with disk-backed caching and periodically refreshes from the source. ## Features - Retrieves indicators of compromise (IOCs) aka. malicious domain names from a MISP server via its REST API. - Handles paginated fetches with exponential backoff and retry on transient failures. - Stores the latest blocklist in memory for fast lookup and persists it to disk for faster startup. - Blocks matching DNS requests by returning NXDOMAIN or, for TXT queries when enabled, a human-readable blocking report. - Optionally includes extended DNS error metadata. - Configurable refresh interval and age window for which indicators are considered. - Optional disabling of TLS certificate validation with explicit warning in logs. ## Configuration Supply a JSON configuration like the following: ```json { "enableBlocking": true, "mispServerUrl": "https://misp.example.com", "mispApiKey": "YourMispApiKeyHere", "disableTlsValidation": false, "updateInterval": "2h", "maxIocAge": "15d", "allowTxtBlockingReport": true, "paginationLimit": 5000, "addExtendedDnsError": true } ``` - You can disable the app without uninstalling. - You can disable TLS validation for test instances and homelabs, but **it is not recommended use this option in production**. - The `maxIocAge` option is used for filtering IOCs wih `lastSeen` attributes on MISP. So, you can dynamically filter for recent campaigns. - The `allowTxtBlockingReport` rewrites the response with a blocking report. - The `addExtendedDnsError` is useful when logs are exported to a SIEM. The blocking report gets added to EDNS payload of the package. # Acknowledgement Thanks to everyone who has been part of or contributed to [MISP Project](https://www.misp-project.org/) for being an amazing resource. ================================================ FILE: Apps/MispConnectorApp/dnsApp.config ================================================ { "enableBlocking": true, "mispServerUrl": "https://misp.example.com", "mispApiKey": "YourMispApiKeyHere", "disableTlsValidation": false, "updateInterval": "2h", "maxIocAge": "30d", "allowTxtBlockingReport": true, "paginationLimit": 5000, "addExtendedDnsError": true } ================================================ FILE: Apps/NoDataApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace NoData { public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler { #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { //do nothing return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; foreach (JsonElement jsonBlockedType in jsonAppRecordData.GetProperty("blockedTypes").EnumerateArray()) { DnsResourceRecordType blockedType = Enum.Parse(jsonBlockedType.GetString(), true); if ((blockedType == question.Type) || (blockedType == DnsResourceRecordType.ANY)) return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question)); } return Task.FromResult(null); } #endregion #region properties public string Description { get { return "Returns a NO DATA response for requests that query for the blocked resource record types in Conditional Forwarder zones."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""blockedTypes"": [ ""A"", ""AAAA"", ""ANY"" ] }"; } } #endregion } } ================================================ FILE: Apps/NoDataApp/NoDataApp.csproj ================================================  net9.0 false 5.0 false Technitium Technitium DNS Server Shreyas Zare NoDataApp NoData https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in Conditional Forwarder zones to return NO DATA response for requests that match the configured query type (QTYPE) to prevent them from being forwarded. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/NoDataApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/NxDomainApp/App.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace NxDomain { public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference { #region variables byte _appPreference; IDnsServer _dnsServer; DnsSOARecordData _soaRecord; bool _enableBlocking; bool _allowTxtBlockingReport; Dictionary _blockListZone; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private bool IsZoneBlocked(string domain, out string blockedDomain) { domain = domain.ToLowerInvariant(); do { if (_blockListZone.TryGetValue(domain, out _)) { //found zone blocked blockedDomain = domain; return true; } domain = GetParentZone(domain); } while (domain is not null); blockedDomain = null; return false; } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; _soaRecord = new DnsSOARecordData(dnsServer.ServerDomain, dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, 60); using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue("appPreference", 20)); _enableBlocking = jsonConfig.GetProperty("enableBlocking").GetBoolean(); _allowTxtBlockingReport = jsonConfig.GetProperty("allowTxtBlockingReport").GetBoolean(); _blockListZone = jsonConfig.ReadArrayAsMap("blocked", delegate (JsonElement jsonDomainName) { return new Tuple(jsonDomainName.GetString(), null); }); return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { if (!_enableBlocking) return Task.FromResult(null); DnsQuestionRecord question = request.Question[0]; if (!IsZoneBlocked(question.Name, out string blockedDomain)) return Task.FromResult(null); if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) { //return meta data DnsResourceRecord[] answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, 60, new DnsTXTRecordData("source=nx-domain-app; domain=" + blockedDomain))]; return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }); } else { EDnsOption[] options = null; if (_allowTxtBlockingReport && (request.EDNS is not null)) options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=nx-domain-app; domain=" + blockedDomain))]; string parentDomain = GetParentZone(blockedDomain); if (parentDomain is null) parentDomain = string.Empty; IReadOnlyList authority = [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, 60, _soaRecord)]; return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }); } } #endregion #region properties public string Description { get { return "Blocks configured domain names with a NX Domain response."; } } public byte Preference { get { return _appPreference; } } #endregion } } ================================================ FILE: Apps/NxDomainApp/NxDomainApp.csproj ================================================  net9.0 false 7.0 false Technitium Technitium DNS Server Shreyas Zare NxDomainApp NxDomain https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Blocks configured domain names with a NX Domain response. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/NxDomainApp/dnsApp.config ================================================ { "appPreference": 20, "enableBlocking": true, "allowTxtBlockingReport": true, "blocked": [ "use-application-dns.net", "mask.icloud.com", "mask-h2.icloud.com" ] } ================================================ FILE: Apps/NxDomainOverrideApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace NxDomainOverride { public sealed class App : IDnsApplication, IDnsPostProcessor { #region variables bool _enableOverride; uint _defaultTtl; Dictionary _domainSetMap; Dictionary _sets; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private bool TryGetMappedSets(string domain, out string[] setNames) { domain = domain.ToLowerInvariant(); string parent; do { if (_domainSetMap.TryGetValue(domain, out setNames)) return true; parent = GetParentZone(domain); if (parent is null) { if (_domainSetMap.TryGetValue("*", out setNames)) return true; break; } domain = "*." + parent; if (_domainSetMap.TryGetValue(domain, out setNames)) return true; domain = parent; } while (true); return false; } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _enableOverride = jsonConfig.GetPropertyValue("enableOverride", true); _defaultTtl = jsonConfig.GetPropertyValue("defaultTtl", 300u); _domainSetMap = jsonConfig.ReadObjectAsMap("domainSetMap", delegate (string domain, JsonElement jsonSets) { string[] sets = jsonSets.GetArray(); return new Tuple(domain.ToLowerInvariant(), sets); }); _sets = jsonConfig.ReadArrayAsMap("sets", delegate (JsonElement jsonSet) { Set set = new Set(jsonSet); return new Tuple(set.Name, set); }); return Task.CompletedTask; } public Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (!_enableOverride) return Task.FromResult(response); if (response.DnssecOk) return Task.FromResult(response); if (response.OPCODE != DnsOpcode.StandardQuery) return Task.FromResult(response); if (response.RCODE != DnsResponseCode.NxDomain) return Task.FromResult(response); DnsQuestionRecord question = request.Question[0]; switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: break; default: //NO DATA response return Task.FromResult(new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, response.Answer, response.Authority, response.Additional) { Tag = response.Tag }); } string nxDomain = question.Name; foreach (DnsResourceRecord record in response.Answer) { if (record.Type == DnsResourceRecordType.CNAME) nxDomain = (record.RDATA as DnsCNAMERecordData).Domain; } if (!TryGetMappedSets(nxDomain, out string[] setNames)) return Task.FromResult(response); List newAnswer = new List(); newAnswer.AddRange(response.Answer); foreach (string setName in setNames) { if (_sets.TryGetValue(setName, out Set set)) { switch (question.Type) { case DnsResourceRecordType.A: foreach (DnsResourceRecordData rdata in set.RecordDataAddresses) { if (rdata is DnsARecordData) newAnswer.Add(new DnsResourceRecord(nxDomain, DnsResourceRecordType.A, DnsClass.IN, _defaultTtl, rdata)); } break; case DnsResourceRecordType.AAAA: foreach (DnsResourceRecordData rdata in set.RecordDataAddresses) { if (rdata is DnsAAAARecordData) newAnswer.Add(new DnsResourceRecord(nxDomain, DnsResourceRecordType.AAAA, DnsClass.IN, _defaultTtl, rdata)); } break; default: throw new InvalidOperationException(); } } } return Task.FromResult(new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, newAnswer) { Tag = response.Tag }); } #endregion #region properties public string Description { get { return "Overrides NX Domain response with custom A/AAAA record response for configured domain names."; } } #endregion class Set { #region variables readonly string _name; readonly DnsResourceRecordData[] _rdataAddresses; #endregion #region constructor public Set(JsonElement jsonSet) { _name = jsonSet.GetProperty("name").GetString(); _rdataAddresses = jsonSet.ReadArray("addresses", delegate (string item) { IPAddress address = IPAddress.Parse(item); switch (address.AddressFamily) { case AddressFamily.InterNetwork: return new DnsARecordData(address); case AddressFamily.InterNetworkV6: return new DnsAAAARecordData(address); default: throw new NotSupportedException("Address family not supported: " + address.AddressFamily.ToString()); } }); } #endregion #region properties public string Name { get { return _name; } } public DnsResourceRecordData[] RecordDataAddresses { get { return _rdataAddresses; } } #endregion } } } ================================================ FILE: Apps/NxDomainOverrideApp/NxDomainOverrideApp.csproj ================================================  net9.0 false 3.0 false Technitium Technitium DNS Server Shreyas Zare NxDomainOverrideApp NxDomainOverride https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Overrides NX Domain response with custom A/AAAA record response for configured domain names. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/NxDomainOverrideApp/dnsApp.config ================================================ { "enableOverride": true, "defaultTtl": 300, "domainSetMap": { "*": ["set1"], "example.com": ["set1", "set2"] }, "sets": [ { "name": "set1", "addresses": [ "192.168.10.1" ] }, { "name": "set2", "addresses": [ "1.2.3.4", "5.6.7.8" ] } ] } ================================================ FILE: Apps/QueryLogsMySqlApp/App.cs ================================================ /* 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.ApplicationCommon; using MySqlConnector; using System; using System.Collections.Generic; using System.Data.Common; using System.Net; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace QueryLogsMySql { public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs { #region variables IDnsServer? _dnsServer; bool _enableLogging; int _maxQueueSize; int _maxLogDays; int _maxLogRecords; string? _databaseName; string? _connectionString; Channel? _channel; ChannelWriter? _channelWriter; Thread? _consumerThread; const int BULK_INSERT_COUNT = 1000; const int BULK_INSERT_ERROR_DELAY = 10000; readonly Timer _cleanupTimer; const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000; const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000; bool _isStartupInit = true; #endregion #region constructor public App() { _cleanupTimer = new Timer(async delegate (object? state) { try { await using (MySqlConnection connection = new MySqlConnection(_connectionString + $" Database={_databaseName};")) { await connection.OpenAsync(); if (_maxLogRecords > 0) { int totalRecords; await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "SELECT Count(*) FROM dns_logs;"; totalRecords = Convert.ToInt32(await command.ExecuteScalarAsync() ?? 0); } int recordsToRemove = totalRecords - _maxLogRecords; if (recordsToRemove > 0) { await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = $"DELETE FROM dns_logs WHERE dlid IN (SELECT * FROM (SELECT dlid FROM dns_logs ORDER BY dlid LIMIT {recordsToRemove}) AS T1);"; await command.ExecuteNonQueryAsync(); } } } if (_maxLogDays > 0) { await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "DELETE FROM dns_logs WHERE timestamp < @timestamp;"; command.Parameters.AddWithValue("@timestamp", DateTime.UtcNow.AddDays(_maxLogDays * -1)); await command.ExecuteNonQueryAsync(); } } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } finally { try { _cleanupTimer?.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _enableLogging = false; //turn off logging _cleanupTimer?.Dispose(); StopChannel(); _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private void StartNewChannel(int maxQueueSize) { ChannelWriter? existingChannelWriter = _channelWriter; //start new channel and consumer thread BoundedChannelOptions options = new BoundedChannelOptions(maxQueueSize); options.SingleWriter = true; options.SingleReader = true; options.FullMode = BoundedChannelFullMode.DropWrite; _channel = Channel.CreateBounded(options); _channelWriter = _channel.Writer; ChannelReader channelReader = _channel.Reader; _consumerThread = new Thread(async delegate () { try { List logs = new List(BULK_INSERT_COUNT); StringBuilder sb = new StringBuilder(4096); while (!_disposed && await channelReader.WaitToReadAsync()) { while (!_disposed && (logs.Count < BULK_INSERT_COUNT) && channelReader.TryRead(out LogEntry log)) { logs.Add(log); } if (logs.Count < 1) continue; await BulkInsertLogsAsync(logs, sb); logs.Clear(); } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } }); _consumerThread.Name = GetType().Name; _consumerThread.IsBackground = true; _consumerThread.Start(); //complete old channel to stop its consumer thread existingChannelWriter?.TryComplete(); } private void StopChannel() { _channel?.Writer.TryComplete(); } private async Task BulkInsertLogsAsync(List logs, StringBuilder sb) { try { await using (MySqlConnection connection = new MySqlConnection(_connectionString + $" Database={_databaseName};")) { await connection.OpenAsync(); await using (MySqlCommand command = connection.CreateCommand()) { sb.Length = 0; sb.Append("INSERT INTO dns_logs (server, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer) VALUES "); for (int i = 0; i < logs.Count; i++) { if (i == 0) sb.Append($"(@server{i}, @timestamp{i}, @client_ip{i}, @protocol{i}, @response_type{i}, @response_rtt{i}, @rcode{i}, @qname{i}, @qtype{i}, @qclass{i}, @answer{i})"); else sb.Append($", (@server{i}, @timestamp{i}, @client_ip{i}, @protocol{i}, @response_type{i}, @response_rtt{i}, @rcode{i}, @qname{i}, @qtype{i}, @qclass{i}, @answer{i})"); } command.CommandText = sb.ToString(); for (int i = 0; i < logs.Count; i++) { LogEntry log = logs[i]; MySqlParameter paramServer = command.Parameters.Add("@server" + i, MySqlDbType.VarChar); MySqlParameter paramTimestamp = command.Parameters.Add("@timestamp" + i, MySqlDbType.DateTime); MySqlParameter paramClientIp = command.Parameters.Add("@client_ip" + i, MySqlDbType.VarChar); MySqlParameter paramProtocol = command.Parameters.Add("@protocol" + i, MySqlDbType.Byte); MySqlParameter paramResponseType = command.Parameters.Add("@response_type" + i, MySqlDbType.Byte); MySqlParameter paramResponseRtt = command.Parameters.Add("@response_rtt" + i, MySqlDbType.Double); MySqlParameter paramRcode = command.Parameters.Add("@rcode" + i, MySqlDbType.Byte); MySqlParameter paramQname = command.Parameters.Add("@qname" + i, MySqlDbType.VarChar); MySqlParameter paramQtype = command.Parameters.Add("@qtype" + i, MySqlDbType.Int16); MySqlParameter paramQclass = command.Parameters.Add("@qclass" + i, MySqlDbType.Int16); MySqlParameter paramAnswer = command.Parameters.Add("@answer" + i, MySqlDbType.VarChar); paramServer.Value = _dnsServer?.ServerDomain; paramTimestamp.Value = log.Timestamp; paramClientIp.Value = log.RemoteEP.Address.ToString(); paramProtocol.Value = (byte)log.Protocol; DnsServerResponseType responseType; if (log.Response.Tag == null) responseType = DnsServerResponseType.Recursive; else responseType = (DnsServerResponseType)log.Response.Tag; paramResponseType.Value = (byte)responseType; if ((responseType == DnsServerResponseType.Recursive) && (log.Response.Metadata is not null)) paramResponseRtt.Value = log.Response.Metadata.RoundTripTime; else paramResponseRtt.Value = DBNull.Value; paramRcode.Value = (byte)log.Response.RCODE; if (log.Request.Question.Count > 0) { DnsQuestionRecord query = log.Request.Question[0]; paramQname.Value = query.Name.ToLowerInvariant(); paramQtype.Value = (short)query.Type; paramQclass.Value = (short)query.Class; } else { paramQname.Value = DBNull.Value; paramQtype.Value = DBNull.Value; paramQclass.Value = DBNull.Value; } if (log.Response.Answer.Count == 0) { paramAnswer.Value = DBNull.Value; } else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer) { paramAnswer.Value = "[ZONE TRANSFER]"; } else { string? answer = null; foreach (DnsResourceRecord record in log.Response.Answer) { if (answer is null) answer = record.Type.ToString() + " " + record.RDATA.ToString(); else answer += ", " + record.Type.ToString() + " " + record.RDATA.ToString(); } if (answer?.Length > 4000) answer = answer.Substring(0, 4000); paramAnswer.Value = answer; } } await command.ExecuteNonQueryAsync(); } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); await Task.Delay(BULK_INSERT_ERROR_DELAY); } } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { try { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; bool enableLogging = jsonConfig.GetPropertyValue("enableLogging", false); int maxQueueSize = jsonConfig.GetPropertyValue("maxQueueSize", 1000000); _maxLogDays = jsonConfig.GetPropertyValue("maxLogDays", 0); _maxLogRecords = jsonConfig.GetPropertyValue("maxLogRecords", 0); _databaseName = jsonConfig.GetPropertyValue("databaseName", "DnsQueryLogs"); _connectionString = jsonConfig.GetPropertyValue("connectionString", null); if (_connectionString is null) throw new Exception("Please specify a valid connection string in 'connectionString' parameter."); if (_connectionString.Replace(" ", "").Contains("Database=", StringComparison.OrdinalIgnoreCase)) throw new Exception("The 'connectionString' parameter must not define 'Database'. Configure the 'databaseName' parameter above instead."); if (!_connectionString.TrimEnd().EndsWith(';')) _connectionString += ";"; async Task ApplyConfig() { if (enableLogging) { await using (MySqlConnection connection = new MySqlConnection(_connectionString)) { await connection.OpenAsync(); await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = @$" CREATE DATABASE IF NOT EXISTS `{_databaseName}`; USE `{_databaseName}`; CREATE TABLE IF NOT EXISTS dns_logs ( dlid INT PRIMARY KEY AUTO_INCREMENT, server varchar(255), timestamp DATETIME NOT NULL, client_ip VARCHAR(39) NOT NULL, protocol TINYINT UNSIGNED NOT NULL, response_type TINYINT NOT NULL, response_rtt REAL, rcode TINYINT NOT NULL, qname VARCHAR(255), qtype SMALLINT, qclass SMALLINT, answer VARCHAR(4000) ); "; await command.ExecuteNonQueryAsync(); } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "ALTER TABLE dns_logs ADD server varchar(255);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "ALTER TABLE dns_logs MODIFY protocol TINYINT UNSIGNED NOT NULL;"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_server ON dns_logs (server);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_timestamp ON dns_logs (timestamp);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_client_ip ON dns_logs (client_ip);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_protocol ON dns_logs (protocol);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_response_type ON dns_logs (response_type);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_rcode ON dns_logs (rcode);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_qname ON dns_logs (qname)"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_qtype ON dns_logs (qtype);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_qclass ON dns_logs (qclass);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_timestamp_client_ip ON dns_logs (timestamp, client_ip);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_timestamp_qname ON dns_logs (timestamp, qname);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_client_qname ON dns_logs (client_ip, qname);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_query ON dns_logs (qname, qtype);"; try { await command.ExecuteNonQueryAsync(); } catch { } } await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX index_all ON dns_logs (server, timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);"; try { await command.ExecuteNonQueryAsync(); } catch { } } } if (!_enableLogging || (_maxQueueSize != maxQueueSize)) StartNewChannel(maxQueueSize); } else { StopChannel(); } _enableLogging = enableLogging; _maxQueueSize = maxQueueSize; if ((_maxLogDays > 0) || (_maxLogRecords > 0)) _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite); else _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); } if (_isStartupInit) { //this is the first time this app is being initialized ThreadPool.QueueUserWorkItem(async delegate (object? state) { try { const int MAX_RETRIES = 20; const int RETRY_DELAY = 30000; //30 seconds int retryCount = 0; while (true) { try { await ApplyConfig(); return; } catch (Exception ex) { if (ex is not MySqlException ex2) { _dnsServer?.WriteLog(ex); return; } switch (ex2.ErrorCode) { case MySqlErrorCode.UnableToConnectToHost: case MySqlErrorCode.TooManyUserConnections: retryCount++; if (retryCount < MAX_RETRIES) { _dnsServer?.WriteLog($"Failed to connect to the database server ({ex2.ErrorCode}). Please check the app config and make sure the database server is online. Retrying in {RETRY_DELAY / 1000} seconds... (Attempt {retryCount})"); _dnsServer?.WriteLog(ex); await Task.Delay(RETRY_DELAY); } else { _dnsServer?.WriteLog($"Failed to connect to the database server ({ex2.ErrorCode}) after {retryCount} retries. Please check the app config and make sure the database server is online."); _dnsServer?.WriteLog(ex); return; } break; default: _dnsServer?.WriteLog($"Failed to connect to the database server ({ex2.ErrorCode}). Please check the app config and make sure the database server is online."); _dnsServer?.WriteLog(ex); return; } } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } }); } else { //init via API call await ApplyConfig(); } } finally { _isStartupInit = false; //reset flag } } public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (_enableLogging) _channelWriter?.TryWrite(new LogEntry(timestamp, request, remoteEP, protocol, response)); return Task.CompletedTask; } public async Task QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass) { if (pageNumber == 0) pageNumber = 1; if (qname is not null) qname = qname.ToLowerInvariant(); string whereClause = $"server = '{_dnsServer?.ServerDomain}' AND "; if (start is not null) whereClause += "timestamp >= @start AND "; if (end is not null) whereClause += "timestamp <= @end AND "; if (clientIpAddress is not null) whereClause += "client_ip = @client_ip AND "; if (protocol is not null) whereClause += "protocol = @protocol AND "; if (responseType is not null) whereClause += "response_type = @response_type AND "; if (rcode is not null) whereClause += "rcode = @rcode AND "; if (qname is not null) { if (qname.Contains('*')) { whereClause += "qname like @qname AND "; qname = qname.Replace("*", "%"); } else { whereClause += "qname = @qname AND "; } } if (qtype is not null) whereClause += "qtype = @qtype AND "; if (qclass is not null) whereClause += "qclass = @qclass AND "; if (!string.IsNullOrEmpty(whereClause)) whereClause = whereClause.Substring(0, whereClause.Length - 5); await using (MySqlConnection connection = new MySqlConnection(_connectionString + $" Database={_databaseName};")) { await connection.OpenAsync(); //find total entries long totalEntries; await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = "SELECT Count(*) FROM dns_logs" + (string.IsNullOrEmpty(whereClause) ? ";" : " WHERE " + whereClause + ";"); if (start is not null) command.Parameters.AddWithValue("@start", start); if (end is not null) command.Parameters.AddWithValue("@end", end); if (clientIpAddress is not null) command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString()); if (protocol is not null) command.Parameters.AddWithValue("@protocol", (byte)protocol); if (responseType is not null) command.Parameters.AddWithValue("@response_type", (byte)responseType); if (rcode is not null) command.Parameters.AddWithValue("@rcode", (byte)rcode); if (qname is not null) command.Parameters.AddWithValue("@qname", qname); if (qtype is not null) command.Parameters.AddWithValue("@qtype", (short)qtype); if (qclass is not null) command.Parameters.AddWithValue("@qclass", (short)qclass); totalEntries = Convert.ToInt64(await command.ExecuteScalarAsync() ?? 0L); } long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0); if ((pageNumber > totalPages) || (pageNumber < 0)) pageNumber = totalPages; long endRowNum; long startRowNum; if (descendingOrder) { endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage); startRowNum = endRowNum - entriesPerPage; } else { endRowNum = pageNumber * entriesPerPage; startRowNum = endRowNum - entriesPerPage; } List entries = new List(entriesPerPage); await using (MySqlCommand command = connection.CreateCommand()) { command.CommandText = @" SELECT * FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY dlid ) row_num, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer FROM dns_logs " + (string.IsNullOrEmpty(whereClause) ? "" : "WHERE " + whereClause) + @" ) t WHERE row_num > @start_row_num AND row_num <= @end_row_num ORDER BY row_num" + (descendingOrder ? " DESC" : ""); command.Parameters.AddWithValue("@start_row_num", startRowNum); command.Parameters.AddWithValue("@end_row_num", endRowNum); if (start is not null) command.Parameters.AddWithValue("@start", start); if (end is not null) command.Parameters.AddWithValue("@end", end); if (clientIpAddress is not null) command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString()); if (protocol is not null) command.Parameters.AddWithValue("@protocol", (byte)protocol); if (responseType is not null) command.Parameters.AddWithValue("@response_type", (byte)responseType); if (rcode is not null) command.Parameters.AddWithValue("@rcode", (byte)rcode); if (qname is not null) command.Parameters.AddWithValue("@qname", qname); if (qtype is not null) command.Parameters.AddWithValue("@qtype", (short)qtype); if (qclass is not null) command.Parameters.AddWithValue("@qclass", (short)qclass); await using (DbDataReader reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { double? responseRtt; if (reader.IsDBNull(5)) responseRtt = null; else responseRtt = reader.GetFloat(5); DnsQuestionRecord? question; if (reader.IsDBNull(7)) question = null; else question = new DnsQuestionRecord(reader.GetString(7), (DnsResourceRecordType)reader.GetInt16(8), (DnsClass)reader.GetInt16(9), false); string? answer; if (reader.IsDBNull(10)) answer = null; else answer = reader.GetString(10); entries.Add(new DnsLogEntry(reader.GetInt64(0), reader.GetDateTime(1), IPAddress.Parse(reader.GetString(2)), (DnsTransportProtocol)reader.GetByte(3), (DnsServerResponseType)reader.GetByte(4), responseRtt, (DnsResponseCode)reader.GetByte(6), question, answer)); } } } return new DnsLogPage(pageNumber, totalPages, totalEntries, entries); } } #endregion #region properties public string Description { get { return "Logs all incoming DNS requests and their responses in a MySQL database that can be queried from the DNS Server web console."; } } #endregion readonly struct LogEntry { #region variables public readonly DateTime Timestamp; public readonly DnsDatagram Request; public readonly IPEndPoint RemoteEP; public readonly DnsTransportProtocol Protocol; public readonly DnsDatagram Response; #endregion #region constructor public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { Timestamp = timestamp; Request = request; RemoteEP = remoteEP; Protocol = protocol; Response = response; } #endregion } } } ================================================ FILE: Apps/QueryLogsMySqlApp/QueryLogsMySqlApp.csproj ================================================  net9.0 false true 3.0.1 false Technitium Technitium DNS Server Shreyas Zare QueryLogsMySqlApp QueryLogsMySql https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Logs all incoming DNS requests and their responses in a MySQL/MariaDB database that can be queried from the DNS Server web console.\n\nNote! You will need to create a user and grant all privileges on the database to the user so that the app will be able to access it. To do that run the following commands with the required database name and username on your mysql root prompt:\nCREATE USER 'user'@'%' IDENTIFIED BY 'password';\nGRANT ALL PRIVILEGES ON DatabaseName.* TO 'user'@'%';\n\nOnce the database is configured, edit the app's config to update the database name, connection string, and set enableLogging to true. The app will automatically create the required database schema for you and start logging queries once you save the config. false Library enable false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll True PreserveNewest ================================================ FILE: Apps/QueryLogsMySqlApp/dnsApp.config ================================================ { "enableLogging": false, "maxQueueSize": 1000000, "maxLogDays": 0, "maxLogRecords": 0, "databaseName": "DnsQueryLogs", "connectionString": "Server=192.168.180.128; Port=3306; Uid=username; Pwd=password;" } ================================================ FILE: Apps/QueryLogsSqlServerApp/App.cs ================================================ /* 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.ApplicationCommon; using Microsoft.Data.SqlClient; using System; using System.Collections.Generic; using System.Data; using System.Net; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace QueryLogsSqlServer { public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs { #region variables IDnsServer? _dnsServer; bool _enableLogging; int _maxQueueSize; int _maxLogDays; int _maxLogRecords; string? _databaseName; string? _connectionString; Channel? _channel; ChannelWriter? _channelWriter; Thread? _consumerThread; const int BULK_INSERT_COUNT = 190; //sql server supports a maximum of 2100 parameters per query const int BULK_INSERT_ERROR_DELAY = 10000; readonly Timer _cleanupTimer; const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000; const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000; bool _isStartupInit = true; #endregion #region constructor public App() { _cleanupTimer = new Timer(async delegate (object? state) { try { await using (SqlConnection connection = new SqlConnection(_connectionString + $" Initial Catalog={_databaseName};")) { await connection.OpenAsync(); if (_maxLogRecords > 0) { int totalRecords; await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "SELECT Count(*) FROM dns_logs;"; totalRecords = Convert.ToInt32(await command.ExecuteScalarAsync() ?? 0); } int recordsToRemove = totalRecords - _maxLogRecords; if (recordsToRemove > 0) { await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = $"DELETE FROM dns_logs WHERE dlid IN (SELECT TOP {recordsToRemove} dlid FROM dns_logs ORDER BY dlid);"; await command.ExecuteNonQueryAsync(); } } } if (_maxLogDays > 0) { await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "DELETE FROM dns_logs WHERE timestamp < @timestamp;"; command.Parameters.AddWithValue("@timestamp", DateTime.UtcNow.AddDays(_maxLogDays * -1)); await command.ExecuteNonQueryAsync(); } } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } finally { try { _cleanupTimer?.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _enableLogging = false; //turn off logging _cleanupTimer?.Dispose(); StopChannel(); _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private void StartNewChannel(int maxQueueSize) { ChannelWriter? existingChannelWriter = _channelWriter; //start new channel and consumer thread BoundedChannelOptions options = new BoundedChannelOptions(maxQueueSize); options.SingleWriter = true; options.SingleReader = true; options.FullMode = BoundedChannelFullMode.DropWrite; _channel = Channel.CreateBounded(options); _channelWriter = _channel.Writer; ChannelReader channelReader = _channel.Reader; _consumerThread = new Thread(async delegate () { try { List logs = new List(BULK_INSERT_COUNT); StringBuilder sb = new StringBuilder(4096); while (!_disposed && await channelReader.WaitToReadAsync()) { while (!_disposed && (logs.Count < BULK_INSERT_COUNT) && channelReader.TryRead(out LogEntry log)) { logs.Add(log); } if (logs.Count < 1) continue; await BulkInsertLogsAsync(logs, sb); logs.Clear(); } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } }); _consumerThread.Name = GetType().Name; _consumerThread.IsBackground = true; _consumerThread.Start(); //complete old channel to stop its consumer thread existingChannelWriter?.TryComplete(); } private void StopChannel() { _channel?.Writer.TryComplete(); } private async Task BulkInsertLogsAsync(List logs, StringBuilder sb) { try { await using (SqlConnection connection = new SqlConnection(_connectionString + $" Initial Catalog={_databaseName};")) { await connection.OpenAsync(); await using (SqlCommand command = connection.CreateCommand()) { sb.Length = 0; sb.Append("INSERT INTO dns_logs (server, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer) VALUES "); for (int i = 0; i < logs.Count; i++) { if (i == 0) sb.Append($"(@server{i}, @timestamp{i}, @client_ip{i}, @protocol{i}, @response_type{i}, @response_rtt{i}, @rcode{i}, @qname{i}, @qtype{i}, @qclass{i}, @answer{i})"); else sb.Append($", (@server{i}, @timestamp{i}, @client_ip{i}, @protocol{i}, @response_type{i}, @response_rtt{i}, @rcode{i}, @qname{i}, @qtype{i}, @qclass{i}, @answer{i})"); } command.CommandText = sb.ToString(); for (int i = 0; i < logs.Count; i++) { LogEntry log = logs[i]; SqlParameter paramServer = command.Parameters.Add("@server" + i, SqlDbType.VarChar); SqlParameter paramTimestamp = command.Parameters.Add("@timestamp" + i, SqlDbType.DateTime); SqlParameter paramClientIp = command.Parameters.Add("@client_ip" + i, SqlDbType.VarChar); SqlParameter paramProtocol = command.Parameters.Add("@protocol" + i, SqlDbType.TinyInt); SqlParameter paramResponseType = command.Parameters.Add("@response_type" + i, SqlDbType.TinyInt); SqlParameter paramResponseRtt = command.Parameters.Add("@response_rtt" + i, SqlDbType.Real); SqlParameter paramRcode = command.Parameters.Add("@rcode" + i, SqlDbType.TinyInt); SqlParameter paramQname = command.Parameters.Add("@qname" + i, SqlDbType.VarChar); SqlParameter paramQtype = command.Parameters.Add("@qtype" + i, SqlDbType.SmallInt); SqlParameter paramQclass = command.Parameters.Add("@qclass" + i, SqlDbType.SmallInt); SqlParameter paramAnswer = command.Parameters.Add("@answer" + i, SqlDbType.VarChar); paramServer.Value = _dnsServer?.ServerDomain; paramTimestamp.Value = log.Timestamp; paramClientIp.Value = log.RemoteEP.Address.ToString(); paramProtocol.Value = (byte)log.Protocol; DnsServerResponseType responseType; if (log.Response.Tag == null) responseType = DnsServerResponseType.Recursive; else responseType = (DnsServerResponseType)log.Response.Tag; paramResponseType.Value = (byte)responseType; if ((responseType == DnsServerResponseType.Recursive) && (log.Response.Metadata is not null)) paramResponseRtt.Value = log.Response.Metadata.RoundTripTime; else paramResponseRtt.Value = DBNull.Value; paramRcode.Value = (byte)log.Response.RCODE; if (log.Request.Question.Count > 0) { DnsQuestionRecord query = log.Request.Question[0]; paramQname.Value = query.Name.ToLowerInvariant(); paramQtype.Value = (short)query.Type; paramQclass.Value = (short)query.Class; } else { paramQname.Value = DBNull.Value; paramQtype.Value = DBNull.Value; paramQclass.Value = DBNull.Value; } if (log.Response.Answer.Count == 0) { paramAnswer.Value = DBNull.Value; } else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer) { paramAnswer.Value = "[ZONE TRANSFER]"; } else { string? answer = null; foreach (DnsResourceRecord record in log.Response.Answer) { if (answer is null) answer = record.Type.ToString() + " " + record.RDATA.ToString(); else answer += ", " + record.Type.ToString() + " " + record.RDATA.ToString(); } if (answer?.Length > 4000) answer = answer.Substring(0, 4000); paramAnswer.Value = answer; } } await command.ExecuteNonQueryAsync(); } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); await Task.Delay(BULK_INSERT_ERROR_DELAY); } } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { try { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; bool enableLogging = jsonConfig.GetPropertyValue("enableLogging", false); int maxQueueSize = jsonConfig.GetPropertyValue("maxQueueSize", 1000000); _maxLogDays = jsonConfig.GetPropertyValue("maxLogDays", 0); _maxLogRecords = jsonConfig.GetPropertyValue("maxLogRecords", 0); _databaseName = jsonConfig.GetPropertyValue("databaseName", "DnsQueryLogs"); _connectionString = jsonConfig.GetPropertyValue("connectionString", null); if (_connectionString is null) throw new Exception("Please specify a valid connection string in 'connectionString' parameter."); if (_connectionString.Contains("Initial Catalog", StringComparison.OrdinalIgnoreCase)) throw new Exception("The 'connectionString' parameter must not define 'Initial Catalog'. Configure the 'databaseName' parameter above instead."); if (!_connectionString.TrimEnd().EndsWith(';')) _connectionString += ";"; async Task ApplyConfig() { if (enableLogging) { await using (SqlConnection connection = new SqlConnection(_connectionString)) { await connection.OpenAsync(); await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = @$" IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = '{_databaseName}') BEGIN CREATE DATABASE ""{_databaseName}""; END "; await command.ExecuteNonQueryAsync(); } await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = @$" USE ""{_databaseName}""; IF NOT EXISTS (SELECT * FROM sys.tables WHERE name='dns_logs' and type='U') BEGIN CREATE TABLE dns_logs ( dlid INT IDENTITY(1,1) PRIMARY KEY, server varchar(255), timestamp DATETIME NOT NULL, client_ip VARCHAR(39) NOT NULL, protocol TINYINT NOT NULL, response_type TINYINT NOT NULL, response_rtt REAL, rcode TINYINT NOT NULL, qname VARCHAR(255), qtype SMALLINT, qclass SMALLINT, answer VARCHAR(4000) ); END IF NOT EXISTS(SELECT * FROM sys.columns WHERE name = 'server' AND object_id = OBJECT_ID('dns_logs')) BEGIN ALTER TABLE dns_logs ADD server varchar(255); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_server' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_server ON dns_logs (server); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_timestamp' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_timestamp ON dns_logs (timestamp); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_client_ip' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_client_ip ON dns_logs (client_ip); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_protocol' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_protocol ON dns_logs (protocol); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_response_type' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_response_type ON dns_logs (response_type); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_rcode' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_rcode ON dns_logs (rcode); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_qname' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_qname ON dns_logs (qname) END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_qtype' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_qtype ON dns_logs (qtype); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_qclass' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_qclass ON dns_logs (qclass); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_timestamp_client_ip' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_timestamp_client_ip ON dns_logs (timestamp, client_ip); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_timestamp_qname' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_timestamp_qname ON dns_logs (timestamp, qname); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_client_qname' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_client_qname ON dns_logs (client_ip, qname); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_query' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_query ON dns_logs (qname, qtype); END IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'index_all' AND object_id = OBJECT_ID('dns_logs')) BEGIN CREATE INDEX index_all ON dns_logs (server, timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass); END "; await command.ExecuteNonQueryAsync(); } } if (!_enableLogging || (_maxQueueSize != maxQueueSize)) StartNewChannel(maxQueueSize); } else { StopChannel(); } _enableLogging = enableLogging; _maxQueueSize = maxQueueSize; if ((_maxLogDays > 0) || (_maxLogRecords > 0)) _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite); else _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); } if (_isStartupInit) { //this is the first time this app is being initialized ThreadPool.QueueUserWorkItem(async delegate (object? state) { try { const int MAX_RETRIES = 20; const int RETRY_DELAY = 30000; //30 seconds int retryCount = 0; while (true) { try { await ApplyConfig(); return; } catch (Exception ex) { if (ex is not SqlException ex2) { _dnsServer?.WriteLog(ex); return; } switch (ex2.Number) { case 258: retryCount++; if (retryCount < MAX_RETRIES) { _dnsServer?.WriteLog($"Failed to connect to the database server ({ex2.Number}). Please check the app config and make sure the database server is online. Retrying in {RETRY_DELAY / 1000} seconds... (Attempt {retryCount})"); _dnsServer?.WriteLog(ex); await Task.Delay(RETRY_DELAY); } else { _dnsServer?.WriteLog($"Failed to connect to the database server ({ex2.Number}) after {retryCount} retries. Please check the app config and make sure the database server is online."); _dnsServer?.WriteLog(ex); return; } break; default: _dnsServer?.WriteLog($"Failed to connect to the database server ({ex2.Number}). Please check the app config and make sure the database server is online."); _dnsServer?.WriteLog(ex); return; } } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } }); } else { //init via API call await ApplyConfig(); } } finally { _isStartupInit = false; //reset flag } } public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (_enableLogging) _channelWriter?.TryWrite(new LogEntry(timestamp, request, remoteEP, protocol, response)); return Task.CompletedTask; } public async Task QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass) { if (pageNumber == 0) pageNumber = 1; if (qname is not null) qname = qname.ToLowerInvariant(); string whereClause = $"server = '{_dnsServer?.ServerDomain}' AND "; if (start is not null) whereClause += "timestamp >= @start AND "; if (end is not null) whereClause += "timestamp <= @end AND "; if (clientIpAddress is not null) whereClause += "client_ip = @client_ip AND "; if (protocol is not null) whereClause += "protocol = @protocol AND "; if (responseType is not null) whereClause += "response_type = @response_type AND "; if (rcode is not null) whereClause += "rcode = @rcode AND "; if (qname is not null) { if (qname.Contains('*')) { whereClause += "qname like @qname AND "; qname = qname.Replace("*", "%"); } else { whereClause += "qname = @qname AND "; } } if (qtype is not null) whereClause += "qtype = @qtype AND "; if (qclass is not null) whereClause += "qclass = @qclass AND "; if (!string.IsNullOrEmpty(whereClause)) whereClause = whereClause.Substring(0, whereClause.Length - 5); await using (SqlConnection connection = new SqlConnection(_connectionString + $" Initial Catalog={_databaseName};")) { await connection.OpenAsync(); //find total entries long totalEntries; await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "SELECT Count(*) FROM dns_logs" + (string.IsNullOrEmpty(whereClause) ? ";" : " WHERE " + whereClause + ";"); if (start is not null) command.Parameters.AddWithValue("@start", start); if (end is not null) command.Parameters.AddWithValue("@end", end); if (clientIpAddress is not null) command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString()); if (protocol is not null) command.Parameters.AddWithValue("@protocol", (byte)protocol); if (responseType is not null) command.Parameters.AddWithValue("@response_type", (byte)responseType); if (rcode is not null) command.Parameters.AddWithValue("@rcode", (byte)rcode); if (qname is not null) command.Parameters.AddWithValue("@qname", qname); if (qtype is not null) command.Parameters.AddWithValue("@qtype", (short)qtype); if (qclass is not null) command.Parameters.AddWithValue("@qclass", (ushort)qclass); totalEntries = Convert.ToInt64(await command.ExecuteScalarAsync() ?? 0L); } long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0); if ((pageNumber > totalPages) || (pageNumber < 0)) pageNumber = totalPages; long endRowNum; long startRowNum; if (descendingOrder) { endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage); startRowNum = endRowNum - entriesPerPage; } else { endRowNum = pageNumber * entriesPerPage; startRowNum = endRowNum - entriesPerPage; } List entries = new List(entriesPerPage); await using (SqlCommand command = connection.CreateCommand()) { command.CommandText = @" SELECT * FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY dlid ) row_num, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer FROM dns_logs " + (string.IsNullOrEmpty(whereClause) ? "" : "WHERE " + whereClause) + @" ) t WHERE row_num > @start_row_num AND row_num <= @end_row_num ORDER BY row_num" + (descendingOrder ? " DESC" : ""); command.Parameters.AddWithValue("@start_row_num", startRowNum); command.Parameters.AddWithValue("@end_row_num", endRowNum); if (start is not null) command.Parameters.AddWithValue("@start", start); if (end is not null) command.Parameters.AddWithValue("@end", end); if (clientIpAddress is not null) command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString()); if (protocol is not null) command.Parameters.AddWithValue("@protocol", (byte)protocol); if (responseType is not null) command.Parameters.AddWithValue("@response_type", (byte)responseType); if (rcode is not null) command.Parameters.AddWithValue("@rcode", (byte)rcode); if (qname is not null) command.Parameters.AddWithValue("@qname", qname); if (qtype is not null) command.Parameters.AddWithValue("@qtype", (short)qtype); if (qclass is not null) command.Parameters.AddWithValue("@qclass", (ushort)qclass); await using (SqlDataReader reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { double? responseRtt; if (reader.IsDBNull(5)) responseRtt = null; else responseRtt = reader.GetFloat(5); DnsQuestionRecord? question; if (reader.IsDBNull(7)) question = null; else question = new DnsQuestionRecord(reader.GetString(7), (DnsResourceRecordType)reader.GetInt16(8), (DnsClass)reader.GetInt16(9), false); string? answer; if (reader.IsDBNull(10)) answer = null; else answer = reader.GetString(10); entries.Add(new DnsLogEntry(reader.GetInt64(0), reader.GetDateTime(1), IPAddress.Parse(reader.GetString(2)), (DnsTransportProtocol)reader.GetByte(3), (DnsServerResponseType)reader.GetByte(4), responseRtt, (DnsResponseCode)reader.GetByte(6), question, answer)); } } } return new DnsLogPage(pageNumber, totalPages, totalEntries, entries); } } #endregion #region properties public string Description { get { return "Logs all incoming DNS requests and their responses in a Microsoft SQL Server database that can be queried from the DNS Server web console."; } } #endregion readonly struct LogEntry { #region variables public readonly DateTime Timestamp; public readonly DnsDatagram Request; public readonly IPEndPoint RemoteEP; public readonly DnsTransportProtocol Protocol; public readonly DnsDatagram Response; #endregion #region constructor public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { Timestamp = timestamp; Request = request; RemoteEP = remoteEP; Protocol = protocol; Response = response; } #endregion } } } ================================================ FILE: Apps/QueryLogsSqlServerApp/QueryLogsSqlServerApp.csproj ================================================  net9.0 false true 2.0 false Technitium Technitium DNS Server Shreyas Zare QueryLogsSqlServerApp QueryLogsSqlServer https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Logs all incoming DNS requests and their responses in a Microsoft SQL Server database that can be queried from the DNS Server web console.\n\nNote! You will need to create a database user, edit the database user properties and enable 'dbcreator' Server Role so that the app is able to create and initialize the database. Once the database is configured, edit the app's config to update the database name, connection string and set enableLogging to true. The app will automatically create the required database schema for you and start logging queries once you save the config. false Library enable false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false PreserveNewest ================================================ FILE: Apps/QueryLogsSqlServerApp/dnsApp.config ================================================ { "enableLogging": false, "maxQueueSize": 1000000, "maxLogDays": 0, "maxLogRecords": 0, "databaseName": "DnsQueryLogs", "connectionString": "Data Source=tcp:192.168.10.101,1433; User ID=username; Password=password; TrustServerCertificate=true;" } ================================================ FILE: Apps/QueryLogsSqliteApp/App.cs ================================================ /* 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.ApplicationCommon; using Microsoft.Data.Sqlite; using System; using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace QueryLogsSqlite { public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs { #region variables IDnsServer? _dnsServer; bool _enableLogging; int _maxQueueSize; int _maxLogDays; int _maxLogRecords; bool _enableVacuum; bool _useInMemoryDb; string? _connectionString; SqliteConnection? _inMemoryConnection; Channel? _channel; ChannelWriter? _channelWriter; Thread? _consumerThread; const int BULK_INSERT_COUNT = 1000; const int BULK_INSERT_ERROR_DELAY = 10000; readonly Timer _cleanupTimer; const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000; const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000; #endregion #region constructor public App() { _cleanupTimer = new Timer(async delegate (object? state) { try { await using (SqliteConnection connection = new SqliteConnection(_connectionString)) { await connection.OpenAsync(); int deletedRecords = 0; if (_maxLogRecords > 0) { await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "DELETE FROM dns_logs WHERE ROWID IN (SELECT ROWID FROM dns_logs ORDER BY ROWID DESC LIMIT -1 OFFSET @maxLogRecords);"; command.Parameters.AddWithValue("@maxLogRecords", _maxLogRecords); deletedRecords += await command.ExecuteNonQueryAsync(); } } if (_maxLogDays > 0) { await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "DELETE FROM dns_logs WHERE timestamp < @timestamp;"; command.Parameters.AddWithValue("@timestamp", DateTime.UtcNow.AddDays(_maxLogDays * -1)); deletedRecords += await command.ExecuteNonQueryAsync(); } } if (_enableVacuum && (deletedRecords > 0)) { await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "VACUUM;"; await command.ExecuteNonQueryAsync(); } } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } finally { try { _cleanupTimer?.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _enableLogging = false; //turn off logging _cleanupTimer?.Dispose(); StopChannel(); if (_inMemoryConnection is not null) { _inMemoryConnection.Dispose(); _inMemoryConnection = null; } SqliteConnection.ClearAllPools(); //close db file _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private void StartNewChannel(int maxQueueSize) { ChannelWriter? existingChannelWriter = _channelWriter; //start new channel and consumer thread BoundedChannelOptions options = new BoundedChannelOptions(maxQueueSize); options.SingleWriter = true; options.SingleReader = true; options.FullMode = BoundedChannelFullMode.DropWrite; _channel = Channel.CreateBounded(options); _channelWriter = _channel.Writer; ChannelReader channelReader = _channel.Reader; _consumerThread = new Thread(async delegate () { try { List logs = new List(BULK_INSERT_COUNT); while (!_disposed && await channelReader.WaitToReadAsync()) { while (!_disposed && (logs.Count < BULK_INSERT_COUNT) && channelReader.TryRead(out LogEntry log)) { logs.Add(log); } if (logs.Count < 1) continue; await BulkInsertLogsAsync(logs); logs.Clear(); } } catch (Exception ex) { _dnsServer?.WriteLog(ex); } }); _consumerThread.Name = GetType().Name; _consumerThread.IsBackground = true; _consumerThread.Start(); //complete old channel to stop its consumer thread existingChannelWriter?.TryComplete(); } private void StopChannel() { _channel?.Writer.TryComplete(); } private async Task BulkInsertLogsAsync(List logs) { try { await using (SqliteConnection connection = new SqliteConnection(_connectionString)) { await connection.OpenAsync(); await using (DbTransaction transaction = await connection.BeginTransactionAsync()) { await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "INSERT INTO dns_logs (timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer) VALUES (@timestamp, @client_ip, @protocol, @response_type, @response_rtt, @rcode, @qname, @qtype, @qclass, @answer);"; SqliteParameter paramTimestamp = command.Parameters.Add("@timestamp", SqliteType.Text); SqliteParameter paramClientIp = command.Parameters.Add("@client_ip", SqliteType.Text); SqliteParameter paramProtocol = command.Parameters.Add("@protocol", SqliteType.Integer); SqliteParameter paramResponseType = command.Parameters.Add("@response_type", SqliteType.Integer); SqliteParameter paramResponseRtt = command.Parameters.Add("@response_rtt", SqliteType.Real); SqliteParameter paramRcode = command.Parameters.Add("@rcode", SqliteType.Integer); SqliteParameter paramQname = command.Parameters.Add("@qname", SqliteType.Text); SqliteParameter paramQtype = command.Parameters.Add("@qtype", SqliteType.Integer); SqliteParameter paramQclass = command.Parameters.Add("@qclass", SqliteType.Integer); SqliteParameter paramAnswer = command.Parameters.Add("@answer", SqliteType.Text); foreach (LogEntry log in logs) { paramTimestamp.Value = log.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFF"); paramClientIp.Value = log.RemoteEP.Address.ToString(); paramProtocol.Value = (int)log.Protocol; DnsServerResponseType responseType; if (log.Response.Tag == null) responseType = DnsServerResponseType.Recursive; else responseType = (DnsServerResponseType)log.Response.Tag; paramResponseType.Value = (int)responseType; if ((responseType == DnsServerResponseType.Recursive) && (log.Response.Metadata is not null)) paramResponseRtt.Value = log.Response.Metadata.RoundTripTime; else paramResponseRtt.Value = DBNull.Value; paramRcode.Value = (int)log.Response.RCODE; if (log.Request.Question.Count > 0) { DnsQuestionRecord query = log.Request.Question[0]; paramQname.Value = query.Name.ToLowerInvariant(); paramQtype.Value = (int)query.Type; paramQclass.Value = (int)query.Class; } else { paramQname.Value = DBNull.Value; paramQtype.Value = DBNull.Value; paramQclass.Value = DBNull.Value; } if (log.Response.Answer.Count == 0) { paramAnswer.Value = DBNull.Value; } else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer) { paramAnswer.Value = "[ZONE TRANSFER]"; } else { string? answer = null; foreach (DnsResourceRecord record in log.Response.Answer) { if (answer is null) answer = record.Type.ToString() + " " + record.RDATA.ToString(); else answer += ", " + record.Type.ToString() + " " + record.RDATA.ToString(); } paramAnswer.Value = answer; } await command.ExecuteNonQueryAsync(); } await transaction.CommitAsync(); } } } } catch (Exception ex) { _dnsServer?.WriteLog(ex); await Task.Delay(BULK_INSERT_ERROR_DELAY); } } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; bool enableLogging = jsonConfig.GetPropertyValue("enableLogging", true); int maxQueueSize = jsonConfig.GetPropertyValue("maxQueueSize", 200000); _maxLogDays = jsonConfig.GetPropertyValue("maxLogDays", 0); _maxLogRecords = jsonConfig.GetPropertyValue("maxLogRecords", 0); _enableVacuum = jsonConfig.GetPropertyValue("enableVacuum", false); _useInMemoryDb = jsonConfig.GetPropertyValue("useInMemoryDb", false); if (_useInMemoryDb) { if (_inMemoryConnection is null) { SqliteConnection.ClearAllPools(); //close db file, if any _connectionString = "Data Source=QueryLogs;Mode=Memory;Cache=Shared"; _inMemoryConnection = new SqliteConnection(_connectionString); await _inMemoryConnection.OpenAsync(); } } else { if (_inMemoryConnection is not null) { await _inMemoryConnection.DisposeAsync(); _inMemoryConnection = null; } string sqliteDbPath = jsonConfig.GetPropertyValue("sqliteDbPath", "querylogs.db"); string connectionString = jsonConfig.GetPropertyValue("connectionString", "Data Source='{sqliteDbPath}'; Cache=Shared;"); if (!Path.IsPathRooted(sqliteDbPath)) sqliteDbPath = Path.Combine(_dnsServer.ApplicationFolder, sqliteDbPath); connectionString = connectionString.Replace("{sqliteDbPath}", sqliteDbPath); if ((_connectionString is not null) && !_connectionString.Equals(connectionString)) SqliteConnection.ClearAllPools(); //close previous db file _connectionString = connectionString; } await using (SqliteConnection connection = new SqliteConnection(_connectionString)) { await connection.OpenAsync(); await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = @" CREATE TABLE IF NOT EXISTS dns_logs ( dlid INTEGER PRIMARY KEY, timestamp DATETIME NOT NULL, client_ip VARCHAR(39) NOT NULL, protocol TINYINT NOT NULL, response_type TINYINT NOT NULL, response_rtt REAL, rcode TINYINT NOT NULL, qname VARCHAR(255), qtype SMALLINT, qclass SMALLINT, answer TEXT ); "; await command.ExecuteNonQueryAsync(); } try { await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "ALTER TABLE dns_logs ADD COLUMN response_rtt REAL;"; await command.ExecuteNonQueryAsync(); } } catch { } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp ON dns_logs (timestamp);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_client_ip ON dns_logs (client_ip);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_protocol ON dns_logs (protocol);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_response_type ON dns_logs (response_type);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_rcode ON dns_logs (rcode);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_qname ON dns_logs (qname);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_qtype ON dns_logs (qtype);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_qclass ON dns_logs (qclass);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp_client_ip ON dns_logs (timestamp, client_ip);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp_qname ON dns_logs (timestamp, qname);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_client_qname ON dns_logs (client_ip, qname);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_query ON dns_logs (qname, qtype);"; await command.ExecuteNonQueryAsync(); } await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "CREATE INDEX IF NOT EXISTS index_all ON dns_logs (timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);"; await command.ExecuteNonQueryAsync(); } } if (enableLogging) { if (!_enableLogging || (_maxQueueSize != maxQueueSize)) StartNewChannel(maxQueueSize); } else { StopChannel(); } _enableLogging = enableLogging; _maxQueueSize = maxQueueSize; if ((_maxLogDays > 0) || (_maxLogRecords > 0)) _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite); else _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); if (!jsonConfig.TryGetProperty("maxLogRecords", out _)) { config = config.Replace("\"sqliteDbPath\"", "\"maxLogRecords\": 0,\r\n \"useInMemoryDb\": false,\r\n \"sqliteDbPath\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } if (!jsonConfig.TryGetProperty("enableVacuum", out _)) { config = config.Replace("\"useInMemoryDb\"", "\"enableVacuum\": false,\r\n \"useInMemoryDb\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } if (!jsonConfig.TryGetProperty("maxQueueSize", out _)) { config = config.Replace("\"maxLogDays\"", "\"maxQueueSize\": 200000,\r\n \"maxLogDays\""); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } } public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (_enableLogging) _channelWriter?.TryWrite(new LogEntry(timestamp, request, remoteEP, protocol, response)); return Task.CompletedTask; } public async Task QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass) { if (pageNumber == 0) pageNumber = 1; if (qname is not null) qname = qname.ToLowerInvariant(); string whereClause = string.Empty; if (start is not null) whereClause += "timestamp >= @start AND "; if (end is not null) whereClause += "timestamp <= @end AND "; if (clientIpAddress is not null) whereClause += "client_ip = @client_ip AND "; if (protocol is not null) whereClause += "protocol = @protocol AND "; if (responseType is not null) whereClause += "response_type = @response_type AND "; if (rcode is not null) whereClause += "rcode = @rcode AND "; if (qname is not null) { if (qname.Contains('*')) { whereClause += "qname like @qname AND "; qname = qname.Replace("*", "%"); } else { whereClause += "qname = @qname AND "; } } if (qtype is not null) whereClause += "qtype = @qtype AND "; if (qclass is not null) whereClause += "qclass = @qclass AND "; if (!string.IsNullOrEmpty(whereClause)) whereClause = whereClause.Substring(0, whereClause.Length - 5); await using (SqliteConnection connection = new SqliteConnection(_connectionString)) { await connection.OpenAsync(); //find total entries long totalEntries; await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = "SELECT Count(*) FROM dns_logs" + (string.IsNullOrEmpty(whereClause) ? ";" : " WHERE " + whereClause + ";"); if (start is not null) command.Parameters.AddWithValue("@start", start); if (end is not null) command.Parameters.AddWithValue("@end", end); if (clientIpAddress is not null) command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString()); if (protocol is not null) command.Parameters.AddWithValue("@protocol", (byte)protocol); if (responseType is not null) command.Parameters.AddWithValue("@response_type", (byte)responseType); if (rcode is not null) command.Parameters.AddWithValue("@rcode", (byte)rcode); if (qname is not null) command.Parameters.AddWithValue("@qname", qname); if (qtype is not null) command.Parameters.AddWithValue("@qtype", (ushort)qtype); if (qclass is not null) command.Parameters.AddWithValue("@qclass", (ushort)qclass); totalEntries = Convert.ToInt64(await command.ExecuteScalarAsync()); } long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0); if ((pageNumber > totalPages) || (pageNumber < 0)) pageNumber = totalPages; long endRowNum; long startRowNum; if (descendingOrder) { endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage); startRowNum = endRowNum - entriesPerPage; } else { endRowNum = pageNumber * entriesPerPage; startRowNum = endRowNum - entriesPerPage; } List entries = new List(entriesPerPage); await using (SqliteCommand command = connection.CreateCommand()) { command.CommandText = @" SELECT * FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY dlid ) row_num, timestamp, client_ip, protocol, response_type, response_rtt, rcode, qname, qtype, qclass, answer FROM dns_logs " + (string.IsNullOrEmpty(whereClause) ? "" : "WHERE " + whereClause) + @" ) t WHERE row_num > @start_row_num AND row_num <= @end_row_num ORDER BY row_num" + (descendingOrder ? " DESC" : ""); command.Parameters.AddWithValue("@start_row_num", startRowNum); command.Parameters.AddWithValue("@end_row_num", endRowNum); if (start is not null) command.Parameters.AddWithValue("@start", start); if (end is not null) command.Parameters.AddWithValue("@end", end); if (clientIpAddress is not null) command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString()); if (protocol is not null) command.Parameters.AddWithValue("@protocol", (byte)protocol); if (responseType is not null) command.Parameters.AddWithValue("@response_type", (byte)responseType); if (rcode is not null) command.Parameters.AddWithValue("@rcode", (byte)rcode); if (qname is not null) command.Parameters.AddWithValue("@qname", qname); if (qtype is not null) command.Parameters.AddWithValue("@qtype", (ushort)qtype); if (qclass is not null) command.Parameters.AddWithValue("@qclass", (ushort)qclass); await using (SqliteDataReader reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { double? responseRtt; if (reader.IsDBNull(5)) responseRtt = null; else responseRtt = reader.GetDouble(5); DnsQuestionRecord? question; if (reader.IsDBNull(7)) question = null; else question = new DnsQuestionRecord(reader.GetString(7), (DnsResourceRecordType)reader.GetInt32(8), (DnsClass)reader.GetInt32(9), false); string? answer; if (reader.IsDBNull(10)) answer = null; else answer = reader.GetString(10); entries.Add(new DnsLogEntry(reader.GetInt64(0), reader.GetDateTime(1), IPAddress.Parse(reader.GetString(2)), (DnsTransportProtocol)reader.GetByte(3), (DnsServerResponseType)reader.GetByte(4), responseRtt, (DnsResponseCode)reader.GetByte(6), question, answer)); } } } return new DnsLogPage(pageNumber, totalPages, totalEntries, entries); } } #endregion #region properties public string Description { get { return "Logs all incoming DNS requests and their responses in a Sqlite database that can be queried from the DNS Server web console."; } } #endregion readonly struct LogEntry { #region variables public readonly DateTime Timestamp; public readonly DnsDatagram Request; public readonly IPEndPoint RemoteEP; public readonly DnsTransportProtocol Protocol; public readonly DnsDatagram Response; #endregion #region constructor public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { Timestamp = timestamp; Request = request; RemoteEP = remoteEP; Protocol = protocol; Response = response; } #endregion } } } ================================================ FILE: Apps/QueryLogsSqliteApp/QueryLogsSqliteApp.csproj ================================================  net9.0 false true 8.0 false Technitium Technitium DNS Server Shreyas Zare QueryLogsSqliteApp QueryLogsSqlite https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Logs all incoming DNS requests and their responses in a Sqlite database that can be queried from the DNS Server web console.\n\nNote! The query logging throughput is limited by the disk throughput on which the Sqlite db file is stored. This app is not recommended to be used with very high throughput (more than 20,000 requests/second).\n\nWarning! When 'enableVacuum' is set to 'true', the app will run 'VACUUM' command after deletion of records which will increase disk IO and may cause the app to not respond for a while. Its recommended to enable this feature periodically, only for a while as needed, to trim the db file size on disk. false Library enable false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false PreserveNewest ================================================ FILE: Apps/QueryLogsSqliteApp/dnsApp.config ================================================ { "enableLogging": true, "maxQueueSize": 200000, "maxLogDays": 7, "maxLogRecords": 10000, "enableVacuum": false, "useInMemoryDb": false, "sqliteDbPath": "querylogs.db", "connectionString": "Data Source='{sqliteDbPath}'; Cache=Shared;" } ================================================ FILE: Apps/SplitHorizonApp/AddressTranslation.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace SplitHorizon { public sealed class AddressTranslation : IDnsApplication, IDnsPostProcessor, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference { #region variables byte _appPreference; bool _enableAddressTranslation; Dictionary _domainGroupMap; Dictionary _networkGroupMap; Dictionary _groups; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { if (string.IsNullOrEmpty(config) || config.StartsWith('#')) { //replace old config with default config config = """ { "networks": { "custom-networks": [ "172.16.1.0/24", "172.16.10.0/24", "172.16.2.1" ] }, "enableAddressTranslation": false, "domainGroupMap": { "example.com": "local1" }, "networkGroupMap": { "10.0.0.0/8": "local1", "172.16.0.0/12": "local2", "192.168.0.0/16": "local3" }, "groups": [ { "name": "local1", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.0/24": "10.0.0.0/24", "5.6.7.8": "10.0.0.5" } }, { "name": "local2", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "172.16.0.4", "5.6.7.8": "172.16.0.5" } }, { "name": "local3", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "192.168.0.4", "5.6.7.8": "192.168.0.5" } } ] } """; await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } do { using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue("appPreference", 40)); if (!jsonConfig.TryGetProperty("enableAddressTranslation", out _)) { //update old config with default config config = config.TrimEnd(' ', '\t', '\r', '\n'); config = config.Substring(0, config.Length - 1); config = config.TrimEnd(' ', '\t', '\r', '\n'); config += """ , "enableAddressTranslation": false, "domainGroupMap": { "example.com": "local1" }, "networkGroupMap": { "10.0.0.0/8": "local1", "172.16.0.0/12": "local2", "192.168.0.0/16": "local3" }, "groups": [ { "name": "local1", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.0/24": "10.0.0.0/24", "5.6.7.8": "10.0.0.5" } }, { "name": "local2", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "172.16.0.4", "5.6.7.8": "172.16.0.5" } }, { "name": "local3", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "192.168.0.4", "5.6.7.8": "192.168.0.5" } } ] } """; await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); //reparse config continue; } _enableAddressTranslation = jsonConfig.GetProperty("enableAddressTranslation").GetBoolean(); if (!jsonConfig.TryReadObjectAsMap("domainGroupMap", delegate (string domain, JsonElement jsonGroupName) { return new Tuple(domain, jsonGroupName.GetString()); }, out _domainGroupMap)) { _domainGroupMap = new Dictionary(1) { { "example.com", "local1" } }; config = config.Replace("\"networkGroupMap\": ", "\"domainGroupMap\": {\r\n \"example.com\": \"local1\"\r\n },\r\n \"networkGroupMap\": "); await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } _networkGroupMap = jsonConfig.ReadObjectAsMap("networkGroupMap", delegate (string strNetworkAddress, JsonElement jsonGroupName) { if (!NetworkAddress.TryParse(strNetworkAddress, out NetworkAddress networkAddress)) throw new InvalidOperationException("Network group map contains an invalid network address: " + strNetworkAddress); return new Tuple(networkAddress, jsonGroupName.GetString()); }); _groups = jsonConfig.ReadArrayAsMap("groups", delegate (JsonElement jsonGroup) { Group group = new Group(jsonGroup); return new Tuple(group.Name, group); }); break; } while (true); } public Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { if (!_enableAddressTranslation) return Task.FromResult(response); if (response.RCODE != DnsResponseCode.NoError) return Task.FromResult(response); DnsQuestionRecord question = request.Question[0]; switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: break; default: return Task.FromResult(response); } if (response.Answer.Count == 0) return Task.FromResult(response); string groupName = null; string qname = question.Name; string domain = null; foreach (KeyValuePair entry in _domainGroupMap) { if ((qname.Equals(entry.Key, StringComparison.OrdinalIgnoreCase) || qname.EndsWith("." + entry.Key, StringComparison.OrdinalIgnoreCase)) && ((domain is null) || (entry.Key.Length > domain.Length))) { domain = entry.Key; groupName = entry.Value; } } if (groupName is null) { IPAddress remoteIP = remoteEP.Address; NetworkAddress network = null; foreach (KeyValuePair entry in _networkGroupMap) { if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) { network = entry.Key; groupName = entry.Value; } } } if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled) return Task.FromResult(response); List newAnswer = new List(response.Answer.Count); foreach (DnsResourceRecord answer in response.Answer) { switch (answer.Type) { case DnsResourceRecordType.A: { IPAddress externalIp = (answer.RDATA as DnsARecordData).Address; if (group.TryExternalToInternalTranslation(externalIp, out IPAddress internalIp)) newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TTL, new DnsARecordData(internalIp))); else newAnswer.Add(answer); } break; case DnsResourceRecordType.AAAA: { IPAddress externalIp = (answer.RDATA as DnsAAAARecordData).Address; if (group.TryExternalToInternalTranslation(externalIp, out IPAddress internalIp)) newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TTL, new DnsAAAARecordData(internalIp))); else newAnswer.Add(answer); } break; default: newAnswer.Add(answer); break; } } return Task.FromResult(response.Clone(newAnswer)); } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { if (!_enableAddressTranslation) return Task.FromResult(null); DnsQuestionRecord question = request.Question[0]; if (question.Type != DnsResourceRecordType.PTR) return Task.FromResult(null); IPAddress remoteIP = remoteEP.Address; NetworkAddress network = null; string groupName = null; foreach (KeyValuePair entry in _networkGroupMap) { if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) { network = entry.Key; groupName = entry.Value; } } if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled || !group.TranslateReverseLookups) return Task.FromResult(null); IPAddress ptrIpAddress = IPAddressExtensions.ParseReverseDomain(question.Name); if (!group.TryInternalToExternalTranslation(ptrIpAddress, out IPAddress externalIp)) return Task.FromResult(null); IReadOnlyList answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, 600, new DnsCNAMERecordData(externalIp.GetReverseDomain())) }; return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer)); } #endregion #region properties public string Description { get { return "Translates IP addresses in DNS response for A & AAAA type request based on the client's network address or configured domain names, and the configured 1:1 translation. Also supports reverse (PTR) queries for translated addresses."; } } public byte Preference { get { return _appPreference; } } #endregion class Group { #region variables readonly string _name; readonly bool _enabled; readonly bool _translateReverseLookups; readonly Dictionary _externalToInternalTranslation; readonly Dictionary _internalToExternalTranslation; readonly List> _externalToInternalNetworkTranslation; #endregion #region constructor public Group(JsonElement jsonGroup) { _name = jsonGroup.GetProperty("name").GetString(); _enabled = jsonGroup.GetProperty("enabled").GetBoolean(); _translateReverseLookups = jsonGroup.GetProperty("translateReverseLookups").GetBoolean(); JsonElement jsonExternalToInternalTranslation = jsonGroup.GetProperty("externalToInternalTranslation"); Dictionary externalToInternalIpTranslation = new Dictionary(); Dictionary internalToExternalIpTranslation = new Dictionary(); List> externalToInternalNetworkTranslation = new List>(); foreach (JsonProperty jsonProperty in jsonExternalToInternalTranslation.EnumerateObject()) { string strExternal = jsonProperty.Name; string strInternal = jsonProperty.Value.GetString(); NetworkAddress external = NetworkAddress.Parse(strExternal); NetworkAddress @internal = NetworkAddress.Parse(strInternal); if (external.AddressFamily != @internal.AddressFamily) throw new InvalidDataException("External to internal translation entries must have same address family: " + strExternal + " - " + strInternal); if (external.PrefixLength != @internal.PrefixLength) throw new InvalidDataException("External to internal translation entries must have same prefix length: " + strExternal + " - " + strInternal); if ( ((external.AddressFamily == AddressFamily.InterNetwork) && (external.PrefixLength == 32)) || ((external.AddressFamily == AddressFamily.InterNetworkV6) && (external.PrefixLength == 128)) ) { externalToInternalIpTranslation.TryAdd(external.Address, @internal.Address); if (_translateReverseLookups) internalToExternalIpTranslation.TryAdd(@internal.Address, external.Address); } else { externalToInternalNetworkTranslation.Add(new KeyValuePair(external, @internal)); } } _externalToInternalTranslation = externalToInternalIpTranslation; if (_translateReverseLookups) _internalToExternalTranslation = internalToExternalIpTranslation; _externalToInternalNetworkTranslation = externalToInternalNetworkTranslation; } #endregion #region public public bool TryExternalToInternalTranslation(IPAddress externalIp, out IPAddress internalIp) { if (_externalToInternalTranslation.TryGetValue(externalIp, out internalIp)) return true; foreach (KeyValuePair networkEntry in _externalToInternalNetworkTranslation) { NetworkAddress external = networkEntry.Key; if (external.AddressFamily != externalIp.AddressFamily) continue; if (external.Contains(externalIp)) { NetworkAddress @internal = networkEntry.Value; switch (external.AddressFamily) { case AddressFamily.InterNetwork: { uint hostMask = ~(0xFFFFFFFFu << (32 - external.PrefixLength)); uint host = externalIp.ConvertIpToNumber() & hostMask; uint addr = @internal.Address.ConvertIpToNumber(); uint internalAddr = addr | host; internalIp = IPAddressExtensions.ConvertNumberToIp(internalAddr); return true; } case AddressFamily.InterNetworkV6: { byte[] externalIpBytes = externalIp.GetAddressBytes(); byte[] internalIpBytes = @internal.Address.GetAddressBytes(); int copyBytes = external.PrefixLength / 8; int balanceBits = external.PrefixLength - (copyBytes * 8); Buffer.BlockCopy(externalIpBytes, copyBytes + 1, internalIpBytes, copyBytes + 1, 16 - copyBytes - 1); if (balanceBits > 0) { int mask = 0xFF << (8 - balanceBits); internalIpBytes[copyBytes] = (byte)((internalIpBytes[copyBytes] & mask) | (externalIpBytes[copyBytes] & ~mask)); } internalIp = new IPAddress(internalIpBytes); return true; } default: throw new InvalidOperationException(); } } } internalIp = null; return false; } public bool TryInternalToExternalTranslation(IPAddress internalIp, out IPAddress externalIp) { if (_internalToExternalTranslation.TryGetValue(internalIp, out externalIp)) return true; foreach (KeyValuePair networkEntry in _externalToInternalNetworkTranslation) { NetworkAddress @internal = networkEntry.Value; if (@internal.AddressFamily != internalIp.AddressFamily) continue; if (@internal.Contains(internalIp)) { NetworkAddress external = networkEntry.Key; switch (@internal.AddressFamily) { case AddressFamily.InterNetwork: { uint hostMask = ~(0xFFFFFFFFu << (32 - @internal.PrefixLength)); uint host = internalIp.ConvertIpToNumber() & hostMask; uint addr = external.Address.ConvertIpToNumber(); uint externalAddr = addr | host; externalIp = IPAddressExtensions.ConvertNumberToIp(externalAddr); return true; } case AddressFamily.InterNetworkV6: { byte[] internalIpBytes = internalIp.GetAddressBytes(); byte[] externalIpBytes = external.Address.GetAddressBytes(); int copyBytes = @internal.PrefixLength / 8; int balanceBits = @internal.PrefixLength - (copyBytes * 8); Buffer.BlockCopy(internalIpBytes, copyBytes + 1, externalIpBytes, copyBytes + 1, 16 - copyBytes - 1); if (balanceBits > 0) { int mask = 0xFF << (8 - balanceBits); externalIpBytes[copyBytes] = (byte)((externalIpBytes[copyBytes] & mask) | (internalIpBytes[copyBytes] & ~mask)); } externalIp = new IPAddress(externalIpBytes); return true; } default: throw new InvalidOperationException(); } } } externalIp = null; return false; } #endregion #region properties public string Name { get { return _name; } } public bool Enabled { get { return _enabled; } } public bool TranslateReverseLookups { get { return _translateReverseLookups; } } #endregion } } } ================================================ FILE: Apps/SplitHorizonApp/README.md ================================================ # Split Horizon The Split Horizon app provides two distinct features which can be used independently: 1. Create `APP` records in primary and forwarder zones that can return different sets of `A`, `AAAA`, or `CNAME` records for clients querying over public, private, or other specified networks. 1. Translate IP addresses in a DNS response for `A` and `AAAA` type requests based on the client's network address and the configured 1:1 translation. The following sections describe each feature in more detail. ## A / AAAA / CNAME To respond with different `A`, `AAAA`, or `CNAME` records to different clients, create an `APP` record with the respective name in a primary or forwarder zone. Select the `Split Horizon` app and the `SplitHorizon.SimpleAddress` class for `A` and `AAAA` records or `SplitHorizon.SimpleCNAME` for `CNAME` records. Each `APP` record is configured with a JSON document which looks like the following: ``` { "public": [ "1.1.1.1", "2.2.2.2" ], "private": [ "192.168.1.1", "::1" ], "custom-networks": [ "172.16.1.1" ], "10.0.0.0/8": [ "10.1.1.1" ] } ``` An example for `CNAME` replacements: ``` { "public": "api.example.com", "private": "api.example.corp", "custom-networks": "custom.example.corp", "10.0.0.0/8": "api.intranet.example.corp" } ``` Keys can be one of the following: - a network specification (like `10.0.0.0/8`) - a named network defined in the global app configuration (see [Address Translation]) - `private`: private IP ranges defined in RFC 1918 - `public`: all IPs outside the private IP ranges defined in RFC 1918 Values are either lists of IPv4 and IPv6 addresses which get mapped to `A` and `AAAA` records in the response or a single string to be used as the resulting `CNAME` response. The lists don't have to be exhaustive: requests from clients which don't match any of the networks are processed as if the `APP` record didn't exist. For example, this could mean that a request is forwarded via a `FWD` record. ## Address Translation Translates IP addresses in a DNS response for `A` and `AAAA` type request based on the client's network address and the configured 1:1 translation. Also supports reverse (`PTR`) queries for translated addresses. ### Configuration This feature is both a _post processor_ as well as a _request handler_. That means, it modifies a response generated by the DNS server before it is sent to the client and also serves authoritative responses for some requests. It is configured globally in the app settings. Its configuration file is a JSON document which looks like the following: ``` { "networks": { "custom-networks": [ "172.16.1.0/24", "172.16.10.0/24", "172.16.2.1" ] }, "enableAddressTranslation": false, "networkGroupMap": { "10.0.0.0/8": "local1", "172.16.0.0/12": "local2", "192.168.0.0/16": "local3" }, "groups": [ { "name": "local1", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.0/24": "10.0.0.0/24", "5.6.7.8": "10.0.0.5" } }, { "name": "local2", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "172.16.0.4", "5.6.7.8": "172.16.0.5" } }, { "name": "local3", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "192.168.0.4", "5.6.7.8": "192.168.0.5" } } ] } ``` The individual settings are: - `networks`: a map of network names to lists of network addresses. This can used to name networks for use in `APP` records for this app. By using named networks, it becomes easy to change a network definition which is reused across multiple `APP` records. - `enableAddressTranslation`: when set to `false`, address translation is disabled and the original response is passed through unmodified. - `networkGroupMap`: maps given networks to one of the following named groups. The IP of the requesting client is usses the group with the most specific network mask. Example: if you have mappings `"192.168.1.0/24" = "local1"` and `"192.168.0.0/16" = "local2"`, the client with the IP `192.168.1.10` is considered a member of group `local1`. - `groups`: a list of groups. A group has the following properties: - `name`: the name of the group. - `enabled`: flag whether to perform address translation for this group. - `translateReverseLookups`: flag whether to respond to `PTR` queries for internal IPs (see below) - `externalToInternalTranslation`: a mapping from external to internal network addresses (see below). The networks must be of the same size (have the same prefix length). ### Processing Forward lookups (`A` and `AAAA`) which fulfill all of the following requirements are processed by this app: - the requesting client is a member of a group defined in `networkGroupMap` - the response code is `NoError` - the response has at least one answer Note that `NXDOMAIN`, `SERVFAIL`, and `NODATA` answers are passed through unmodified. For every `A` and `AAAA` record in the response, the app replaces any IP according to the rules defined in `externalToInternalTranslation` of the client's network group. The translation is a 1:1 mapping which just replaces the network part of an IP. For example, given `externalToInternalTranslation": { "1.2.3.0/24": "10.0.0.0/24 }`, the response `1.2.3.4` is replaced with `10.0.0.4`. If `translateReverseLookups` is enabled for a given group, the app also modifies `PTR` queries for domains representing the internal IPs of the group by responding with a `CNAME` record for a domain representing the corresponding IP in the external range. For example, given `externalToInternalTranslation": { "1.2.3.0/24": "10.0.0.0/24 }`, a `PTR` query for `4.0.0.10.in-addr.arpa` will receive the response `CNAME 4.3.2.1.in-addr.arpa`. ================================================ FILE: Apps/SplitHorizonApp/SimpleAddress.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace SplitHorizon { public sealed class SimpleAddress : IDnsApplication, IDnsAppRecordRequestHandler { #region variables static Dictionary> _networks; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public async Task InitializeAsync(IDnsServer dnsServer, string config) { if (string.IsNullOrEmpty(config) || config.StartsWith('#')) { //replace old config with default config config = """ { "networks": { "custom-networks": [ "172.16.1.0/24", "172.16.10.0/24", "172.16.2.1" ] }, "enableAddressTranslation": false, "networkGroupMap": { "10.0.0.0/8": "local1", "172.16.0.0/12": "local2", "192.168.0.0/16": "local3" }, "groups": [ { "name": "local1", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "10.0.0.4", "5.6.7.8": "10.0.0.5" } }, { "name": "local2", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "172.16.0.4", "5.6.7.8": "172.16.0.5" } }, { "name": "local3", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "192.168.0.4", "5.6.7.8": "192.168.0.5" } } ] } """; await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; if (jsonConfig.TryGetProperty("networks", out JsonElement jsonNetworks)) { Dictionary> networks = new Dictionary>(); foreach (JsonProperty jsonProperty in jsonNetworks.EnumerateObject()) { string networkName = jsonProperty.Name; JsonElement jsonNetworkAddresses = jsonProperty.Value; if (jsonNetworkAddresses.ValueKind == JsonValueKind.Array) { List networkAddresses = new List(jsonNetworkAddresses.GetArrayLength()); foreach (JsonElement jsonNetworkAddress in jsonNetworkAddresses.EnumerateArray()) networkAddresses.Add(NetworkAddress.Parse(jsonNetworkAddress.GetString())); networks.TryAdd(networkName, networkAddresses); } } _networks = networks; } else { _networks = new Dictionary>(1); } } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonAddresses = default; NetworkAddress selectedNetwork = null; foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject()) { string name = jsonProperty.Name; if ((name == "public") || (name == "private")) continue; if (_networks.TryGetValue(name, out List networkAddresses)) { foreach (NetworkAddress networkAddress in networkAddresses) { if (networkAddress.Contains(remoteEP.Address)) { jsonAddresses = jsonProperty.Value; break; } } if (jsonAddresses.ValueKind != JsonValueKind.Undefined) break; } else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress)) { if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength))) { selectedNetwork = networkAddress; jsonAddresses = jsonProperty.Value; } } } if (jsonAddresses.ValueKind == JsonValueKind.Undefined) { if (NetUtilities.IsPrivateIP(remoteEP.Address)) { if (!jsonAppRecordData.TryGetProperty("private", out jsonAddresses)) return Task.FromResult(null); } else { if (!jsonAppRecordData.TryGetProperty("public", out jsonAddresses)) return Task.FromResult(null); } } List answers = new List(); switch (question.Type) { case DnsResourceRecordType.A: foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork)) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address))); } break; case DnsResourceRecordType.AAAA: foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray()) { if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6)) answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address))); } break; } if (answers.Count == 0) return Task.FromResult(null); if (answers.Count > 1) answers.Shuffle(); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); } default: return Task.FromResult(null); } } #endregion #region properties internal static Dictionary> Networks { get { return _networks; } } public string Description { get { return "Returns A or AAAA records with different set of IP addresses for clients querying over public, private, or other specified networks."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""public"": [ ""1.1.1.1"", ""2.2.2.2"" ], ""private"": [ ""192.168.1.1"", ""::1"" ], ""custom-networks"": [ ""172.16.1.1"" ], ""10.0.0.0/8"": [ ""10.1.1.1"" ] }"; } } #endregion } } ================================================ FILE: Apps/SplitHorizonApp/SimpleCNAME.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace SplitHorizon { public sealed class SimpleCNAME : IDnsApplication, IDnsAppRecordRequestHandler { #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { //SimpleAddress loads the shared config return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); using JsonDocument jsonDocument = JsonDocument.Parse(appRecordData); JsonElement jsonAppRecordData = jsonDocument.RootElement; JsonElement jsonCname = default; NetworkAddress selectedNetwork = null; foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject()) { string name = jsonProperty.Name; if ((name == "public") || (name == "private")) continue; if (SimpleAddress.Networks.TryGetValue(name, out List networkAddresses)) { foreach (NetworkAddress networkAddress in networkAddresses) { if (networkAddress.Contains(remoteEP.Address)) { jsonCname = jsonProperty.Value; break; } } if (jsonCname.ValueKind != JsonValueKind.Undefined) break; } else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress)) { if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength))) { selectedNetwork = networkAddress; jsonCname = jsonProperty.Value; } } } if (jsonCname.ValueKind == JsonValueKind.Undefined) { if (NetUtilities.IsPrivateIP(remoteEP.Address)) { if (!jsonAppRecordData.TryGetProperty("private", out jsonCname)) return Task.FromResult(null); } else { if (!jsonAppRecordData.TryGetProperty("public", out jsonCname)) return Task.FromResult(null); } } string cname = jsonCname.GetString(); if (string.IsNullOrEmpty(cname)) return Task.FromResult(null); IReadOnlyList answers; if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(cname)) }; //use ANAME else answers = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(cname)) }; return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers)); } #endregion #region properties public string Description { get { return "Returns different CNAME record for clients querying over public, private, or other specified networks. Note that the app will return ANAME record for an APP record at zone apex."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""public"": ""api.example.com"", ""private"": ""api.example.corp"", ""custom-networks"": ""custom.example.corp"", ""10.0.0.0/8"": ""api.intranet.example.corp"" }"; } } #endregion } } ================================================ FILE: Apps/SplitHorizonApp/SplitHorizonApp.csproj ================================================  net9.0 false 10.0 false Technitium Technitium DNS Server Shreyas Zare SplitHorizonApp SplitHorizon https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return different set of A or AAAA records, or CNAME record for clients querying over public, private, or other specified networks.\n\nEnables Address Translation of IP addresses in a DNS response for A & AAAA type request based on the client's network address or configured domain names, and the configured 1:1 translation. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/SplitHorizonApp/dnsApp.config ================================================ { "appPreference": 40, "networks": { "custom-networks": [ "172.16.1.0/24", "172.16.10.0/24", "172.16.2.1" ] }, "enableAddressTranslation": false, "domainGroupMap": { "example.com": "local1" }, "networkGroupMap": { "10.0.0.0/8": "local1", "172.16.0.0/12": "local2", "192.168.0.0/16": "local3" }, "groups": [ { "name": "local1", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.0/24": "10.0.0.0/24", "5.6.7.8": "10.0.0.5" } }, { "name": "local2", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "172.16.0.4", "5.6.7.8": "172.16.0.5" } }, { "name": "local3", "enabled": true, "translateReverseLookups": true, "externalToInternalTranslation": { "1.2.3.4": "192.168.0.4", "5.6.7.8": "192.168.0.5" } } ] } ================================================ FILE: Apps/WeightedRoundRobinApp/Address.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Security.Cryptography; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace WeightedRoundRobin { public sealed class Address : IDnsApplication, IDnsAppRecordRequestHandler { #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); string jsonPropertyName; switch (question.Type) { case DnsResourceRecordType.A: jsonPropertyName = "ipv4Addresses"; break; case DnsResourceRecordType.AAAA: jsonPropertyName = "ipv6Addresses"; break; default: return Task.FromResult(null); } List addresses; int totalWeight = 0; using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; if (!jsonAppRecordData.TryGetProperty(jsonPropertyName, out JsonElement jsonAddresses) || (jsonAddresses.ValueKind == JsonValueKind.Null)) return Task.FromResult(null); addresses = new List(jsonAddresses.GetArrayLength()); foreach (JsonElement jsonAddressEntry in jsonAddresses.EnumerateArray()) { if (jsonAddressEntry.TryGetProperty("enabled", out JsonElement jsonEnabled) && (jsonEnabled.ValueKind != JsonValueKind.Null) && !jsonEnabled.GetBoolean()) continue; if (!jsonAddressEntry.TryGetProperty("address", out JsonElement jsonAddress) || (jsonAddress.ValueKind == JsonValueKind.Null) || !IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address)) continue; if (!jsonAddressEntry.TryGetProperty("weight", out JsonElement jsonWeight) || (jsonWeight.ValueKind == JsonValueKind.Null)) continue; int weight = jsonWeight.GetInt32(); if (weight < 1) continue; addresses.Add(new WeightedAddress() { Address = address, Weight = weight }); totalWeight += weight; } } if (addresses.Count == 0) return Task.FromResult(null); int randomSelection = RandomNumberGenerator.GetInt32(1, 101); int rangeFrom; int rangeTo = 0; DnsResourceRecord answer = null; for (int i = 0; i < addresses.Count; i++) { rangeFrom = rangeTo + 1; if (i == addresses.Count - 1) rangeTo = 100; else rangeTo += addresses[i].Weight * 100 / totalWeight; if ((rangeFrom <= randomSelection) && (randomSelection <= rangeTo)) { switch (question.Type) { case DnsResourceRecordType.A: answer = new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, appRecordTtl, new DnsARecordData(addresses[i].Address)); break; case DnsResourceRecordType.AAAA: answer = new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(addresses[i].Address)); break; default: throw new InvalidOperationException(); } break; } } if (answer is null) throw new InvalidOperationException(); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); } #endregion #region properties public string Description { get { return "Returns an A or AAAA record using weighted round-robin load balancing."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""ipv4Addresses"": [ { ""address"": ""1.1.1.1"", ""weight"": 5, ""enabled"": true }, { ""address"": ""2.2.2.2"", ""weight"": 3, ""enabled"": true } ], ""ipv6Addresses"": [ { ""address"": ""::1"", ""weight"": 2, ""enabled"": true }, { ""address"": ""::2"", ""weight"": 3, ""enabled"": true } ] }"; } } #endregion struct WeightedAddress { public IPAddress Address; public int Weight; } } } ================================================ FILE: Apps/WeightedRoundRobinApp/CNAME.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Security.Cryptography; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace WeightedRoundRobin { public sealed class CNAME : IDnsApplication, IDnsAppRecordRequestHandler { #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); List domainNames; int totalWeight = 0; using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) { JsonElement jsonAppRecordData = jsonDocument.RootElement; if (!jsonAppRecordData.TryGetProperty("cnames", out JsonElement jsonCnames) || (jsonCnames.ValueKind == JsonValueKind.Null)) return Task.FromResult(null); domainNames = new List(jsonCnames.GetArrayLength()); foreach (JsonElement jsonCnameEntry in jsonCnames.EnumerateArray()) { if (jsonCnameEntry.TryGetProperty("enabled", out JsonElement jsonEnabled) && (jsonEnabled.ValueKind != JsonValueKind.Null) && !jsonEnabled.GetBoolean()) continue; if (!jsonCnameEntry.TryGetProperty("domain", out JsonElement jsonDomain) || (jsonDomain.ValueKind == JsonValueKind.Null)) continue; if (!jsonCnameEntry.TryGetProperty("weight", out JsonElement jsonWeight) || (jsonWeight.ValueKind == JsonValueKind.Null)) continue; int weight = jsonWeight.GetInt32(); if (weight < 1) continue; domainNames.Add(new WeightedDomain() { Domain = jsonDomain.GetString(), Weight = weight }); totalWeight += weight; } } if (domainNames.Count == 0) return Task.FromResult(null); int randomSelection = RandomNumberGenerator.GetInt32(1, 101); int rangeFrom; int rangeTo = 0; DnsResourceRecord answer = null; for (int i = 0; i < domainNames.Count; i++) { rangeFrom = rangeTo + 1; if (i == domainNames.Count - 1) rangeTo = 100; else rangeTo += domainNames[i].Weight * 100 / totalWeight; if ((rangeFrom <= randomSelection) && (randomSelection <= rangeTo)) { if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(domainNames[i].Domain)); //use ANAME else answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(domainNames[i].Domain)); break; } } if (answer is null) throw new InvalidOperationException(); return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); } #endregion #region properties public string Description { get { return "Returns a CNAME record using weighted round-robin load balancing."; } } public string ApplicationRecordDataTemplate { get { return @"{ ""cnames"": [ { ""domain"": ""example.com"", ""weight"": 5, ""enabled"": true }, { ""domain"": ""example.net"", ""weight"": 3, ""enabled"": true } ] }"; } } #endregion struct WeightedDomain { public string Domain; public int Weight; } } } ================================================ FILE: Apps/WeightedRoundRobinApp/WeightedRoundRobinApp.csproj ================================================  net9.0 false 4.0 false Technitium Technitium DNS Server Shreyas Zare WeightedRoundRobinApp WeightedRoundRobin https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record using weighted round-robin load balancing. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/WeightedRoundRobinApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/WhatIsMyDnsApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace WhatIsMyDns { public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler { #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { //do nothing return Task.CompletedTask; } public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { DnsQuestionRecord question = request.Question[0]; if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase) && !appRecordName.StartsWith('*')) return Task.FromResult(null); DnsResourceRecord answer; switch (question.Type) { case DnsResourceRecordType.A: if (remoteEP.AddressFamily != AddressFamily.InterNetwork) return Task.FromResult(null); answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(remoteEP.Address)); break; case DnsResourceRecordType.AAAA: if (remoteEP.AddressFamily != AddressFamily.InterNetworkV6) return Task.FromResult(null); answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(remoteEP.Address)); break; case DnsResourceRecordType.TXT: answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, DnsClass.IN, appRecordTtl, new DnsTXTRecordData(remoteEP.Address.ToString())); break; default: return Task.FromResult(null); } return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); } #endregion #region properties public string Description { get { return "Returns the IP address of the user's DNS Server for A, AAAA, and TXT queries."; } } public string ApplicationRecordDataTemplate { get { return null; } } #endregion } } ================================================ FILE: Apps/WhatIsMyDnsApp/WhatIsMyDnsApp.csproj ================================================  net9.0 false true 8.0 false Technitium Technitium DNS Server Shreyas Zare WhatIsMyDnsApp WhatIsMyDns https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return the IP address of the user's DNS Server for A, AAAA, and TXT queries. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/WhatIsMyDnsApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/WildIpApp/App.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.ApplicationCommon; using System; using System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace WildIp { public sealed class App : IDnsApplication, IDnsAppRecordRequestHandler { #region variables static readonly char[] aRecordSeparator = new char[] { '.', '-' }; static readonly char[] aaaaRecordSeparator = new char[] { '.' }; IDnsServer _dnsServer; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; return Task.CompletedTask; } public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) { string qname = request.Question[0].Name; if (qname.Length == appRecordName.Length) return null; DnsResourceRecord answer = null; switch (request.Question[0].Type) { case DnsResourceRecordType.A: { string subdomain = qname.Substring(0, qname.Length - appRecordName.Length); string[] parts = subdomain.Split(aRecordSeparator, StringSplitOptions.RemoveEmptyEntries); byte[] rawIp = new byte[4]; int i = 0; for (int j = 0; (j < parts.Length) && (i < 4); j++) { if (byte.TryParse(parts[j], out byte x)) rawIp[i++] = x; } if (i == 4) answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(new IPAddress(rawIp))); } break; case DnsResourceRecordType.AAAA: { string subdomain = qname.Substring(0, qname.Length - appRecordName.Length - 1); string[] parts = subdomain.Split(aaaaRecordSeparator, StringSplitOptions.RemoveEmptyEntries); IPAddress address = null; foreach (string part in parts) { if (part.Contains('-') && IPAddress.TryParse(part.Replace('-', ':'), out address)) { break; } else if (part.Length == 32) { string addr = null; for (int i = 0; i < 32; i += 4) { if (addr is null) addr = part.Substring(i, 4); else addr += string.Concat(":", part.AsSpan(i, 4)); } if (IPAddress.TryParse(addr, out address)) break; } } if (address is not null) answer = new DnsResourceRecord(request.Question[0].Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)); } break; } if (answer is null) { //NODATA reponse DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(zoneName, DnsResourceRecordType.SOA, DnsClass.IN)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, null, soaResponse.Answer); } return new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer }); } #endregion #region properties public string Description { get { return "Returns the IP address that was embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io."; } } public string ApplicationRecordDataTemplate { get { return null; } } #endregion } } ================================================ FILE: Apps/WildIpApp/WildIpApp.csproj ================================================  net9.0 false true 5.0 false Technitium Technitium DNS Server Shreyas Zare WildIpApp WildIp https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows creating APP records in primary and forwarder zones that can return the IP address embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/WildIpApp/dnsApp.config ================================================ #This app requires no config. ================================================ FILE: Apps/ZoneAliasApp/App.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace ZoneAlias { public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler, IDnsApplicationPreference { #region variables IDnsServer _dnsServer; byte _appPreference; bool _enableAliasing; Dictionary _aliases; #endregion #region IDisposable public void Dispose() { //do nothing } #endregion #region private private static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } private bool IsZoneAlias(string domain, out string zone, out string alias) { domain = domain.ToLowerInvariant(); do { if (_aliases.TryGetValue(domain, out zone)) { //found alias alias = domain; return true; } domain = GetParentZone(domain); } while (domain is not null); alias = null; return false; } private static IReadOnlyList ConvertRecords(IReadOnlyList records, string zone, string alias) { if (records.Count == 0) return records; DnsResourceRecord[] newRecords = new DnsResourceRecord[records.Count]; int j; for (int i = 0; i < records.Count; i++) { DnsResourceRecord record = records[i]; j = record.Name.LastIndexOf(zone, StringComparison.OrdinalIgnoreCase); if (j == 0) newRecords[i] = new DnsResourceRecord(alias, record.Type, record.Class, record.TTL, record.RDATA); else if (j > 0) newRecords[i] = new DnsResourceRecord(string.Concat(record.Name.AsSpan(0, j), alias), record.Type, record.Class, record.TTL, record.RDATA); else newRecords[i] = record; } return newRecords; } #endregion #region public public Task InitializeAsync(IDnsServer dnsServer, string config) { _dnsServer = dnsServer; using JsonDocument jsonDocument = JsonDocument.Parse(config); JsonElement jsonConfig = jsonDocument.RootElement; _appPreference = Convert.ToByte(jsonConfig.GetPropertyValue("appPreference", 10)); _enableAliasing = jsonConfig.GetPropertyValue("enableAliasing", true); if (jsonConfig.TryGetProperty("zoneAliases", out JsonElement jsonZoneAliases)) { Dictionary aliases = new Dictionary(); foreach (JsonProperty jsonZoneAlias in jsonZoneAliases.EnumerateObject()) { string zone = jsonZoneAlias.Name.ToLowerInvariant(); foreach (JsonElement jsonAlias in jsonZoneAlias.Value.EnumerateArray()) aliases.Add(jsonAlias.GetString().ToLowerInvariant(), zone); } aliases.TrimExcess(); _aliases = aliases; } else { _aliases = null; } return Task.CompletedTask; } public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { if (!_enableAliasing || (_aliases is null)) return null; DnsQuestionRecord question = request.Question[0]; string qname = question.Name; if (!IsZoneAlias(qname, out string zone, out string alias)) return null; string newQname; int i = qname.LastIndexOf(alias, StringComparison.OrdinalIgnoreCase); if (i == 0) newQname = zone; else if (i > 0) newQname = string.Concat(qname.AsSpan(0, i), zone); else return null; DnsQuestionRecord newQuestion = new DnsQuestionRecord(newQname, question.Type, question.Class); try { DnsDatagram response = await _dnsServer.DirectQueryAsync(newQuestion); IReadOnlyList newAnswer = ConvertRecords(response.Answer, zone, alias); IReadOnlyList newAuthority = ConvertRecords(response.Authority, zone, alias); IReadOnlyList newAdditional = ConvertRecords(response.Additional, zone, alias); return new DnsDatagram(request.Identifier, true, request.OPCODE, response.AuthoritativeAnswer, response.Truncation, request.RecursionDesired, isRecursionAllowed, false, false, response.RCODE, request.Question, newAnswer, newAuthority, newAdditional) { Tag = response.Tag }; } catch (TimeoutException) { } catch (Exception ex) { _dnsServer.WriteLog(ex); } return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.ServerFailure, request.Question); } #endregion #region properties public string Description { get { return "Allows configuring aliases for any zone (internal or external) such that they all return the same set of records."; } } public byte Preference { get { return _appPreference; } } #endregion } } ================================================ FILE: Apps/ZoneAliasApp/ZoneAliasApp.csproj ================================================  net9.0 false 4.0 false Technitium Technitium DNS Server Shreyas Zare ZoneAliasApp ZoneAlias https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer Allows configuring aliases for any zone (internal or external) such that they all return the same set of records. false Library false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll false ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false PreserveNewest ================================================ FILE: Apps/ZoneAliasApp/dnsApp.config ================================================ { "appPreference": 10, "enableAliasing": true, "zoneAliases": { "example.com": ["example.net", "example.org"] } } ================================================ FILE: Apps/apps2.json ================================================ [ { "name": "Advanced Blocking", "description": "Blocks domain names using block lists and regex block lists. Supports creating groups based on client's IP address or subnet to enforce different block lists and regex block lists for each group.\n\nNote! This app works independent of the DNS server's built-in blocking feature. The options configured in DNS server Settings section does not apply to this app.", "versions": [ { "serverVersion": "9.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v3.zip", "size": "28.3 KB" }, { "serverVersion": "10.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v4.zip", "size": "28.6 KB" }, { "serverVersion": "10.0.1", "version": "4.0.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v4.0.1.zip", "size": "28.9 KB" }, { "serverVersion": "11.0", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v5.0.1.zip", "size": "27.4 KB" }, { "serverVersion": "11.0.3", "version": "5.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v5.1.zip", "size": "27.8 KB" }, { "serverVersion": "11.1", "version": "5.1.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v5.1.1.zip", "size": "27.9 KB" }, { "serverVersion": "11.5", "version": "6.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v6.zip", "size": "28.2 KB" }, { "serverVersion": "11.5.1", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v6.0.1.zip", "size": "28.2 KB" }, { "serverVersion": "11.5.3", "version": "6.1.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v6.1.1.zip", "size": "29.6 KB" }, { "serverVersion": "12.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v7.zip", "size": "30.4 KB" }, { "serverVersion": "12.1", "version": "7.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v7.1.zip", "size": "30.8 KB" }, { "serverVersion": "12.2", "version": "7.1.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v7.1.1.zip", "size": "30.7 KB" }, { "serverVersion": "13.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v8.zip", "size": "30.7 KB" }, { "serverVersion": "14.0", "version": "9.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v9.zip", "size": "31.0 KB" }, { "serverVersion": "14.2.0", "version": "9.1", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v9.1.zip", "size": "33.08 KB" }, { "serverVersion": "14.3", "version": "10.0", "url": "https://download.technitium.com/dns/apps/AdvancedBlockingApp-v10.zip", "size": "33.12 KB" } ] }, { "name": "Advanced Forwarding", "description": "Provides advanced, bulk conditional forwarding options. Supports creating groups based on client's IP address or subnet to enable different conditional forwarding configuration for each group. Supports AdGuard Upstreams config files.", "versions": [ { "serverVersion": "11.0", "version": "1.0.1", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.0.1.zip", "size": "19.1 KB" }, { "serverVersion": "11.0.3", "version": "1.0.2", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.0.2.zip", "size": "19.2 KB" }, { "serverVersion": "11.1", "version": "1.1", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.1.zip", "size": "19.3 KB" }, { "serverVersion": "11.5", "version": "1.2", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v1.2.zip", "size": "19.3 KB" }, { "serverVersion": "12.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v2.zip", "size": "20.1 KB" }, { "serverVersion": "12.1", "version": "2.1", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v2.1.zip", "size": "20.5 KB" }, { "serverVersion": "13.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v3.zip", "size": "20.4 KB" }, { "serverVersion": "13.5", "version": "3.1", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v3.1.zip", "size": "20.5 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/AdvancedForwardingApp-v4.zip", "size": "20.7 KB" } ] }, { "name": "Auto PTR", "description": "Allows creating APP records in primary and forwarder zones that can return automatically generated response for a PTR request for both IPv4 and IPv6.", "versions": [ { "serverVersion": "11.2", "version": "1.0", "url": "https://download.technitium.com/dns/apps/AutoPtrApp-v1.zip", "size": "10.6 KB" }, { "serverVersion": "11.3", "version": "1.0.2", "url": "https://download.technitium.com/dns/apps/AutoPtrApp-v1.0.2.zip", "size": "11.4 KB" }, { "serverVersion": "12.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/AutoPtrApp-v2.zip", "size": "12.3 KB" }, { "serverVersion": "13.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/AutoPtrApp-v3.zip", "size": "12.2 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/AutoPtrApp-v4.zip", "size": "12.3 KB" } ] }, { "name": "Block Page", "description": "Serves a block page from a built-in web server that can be displayed to the end user when a website is blocked by the DNS server.\n\nNote! You need to manually set the Blocking Type as Custom Address in the blocking settings and configure the current server's IP address as Custom Blocking Addresses for the block page to be served to the users. Use a PKCS #12 certificate (.pfx or .p12) for enabling HTTPS support. Enabling HTTPS support will show certificate error to the user which is expected and the user will have to proceed ignoring the certificate error to be able to see the block page.", "versions": [ { "serverVersion": "9.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v2.zip", "size": "21.2 KB" }, { "serverVersion": "10.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v3.zip", "size": "22.1 KB" }, { "serverVersion": "10.0.1", "version": "3.0.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v3.0.1.zip", "size": "22.1 KB" }, { "serverVersion": "11.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v4.zip", "size": "20.6 KB" }, { "serverVersion": "11.0.3", "version": "4.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v4.1.zip", "size": "20.9 KB" }, { "serverVersion": "11.3", "version": "4.2", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v4.2.zip", "size": "21.1 KB" }, { "serverVersion": "11.5.3", "version": "4.3", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v4.3.zip", "size": "23.6 KB" }, { "serverVersion": "11.5.3", "version": "4.3.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v4.3.1.zip", "size": "23.6 KB" }, { "serverVersion": "12.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v5.zip", "size": "25.1 KB" }, { "serverVersion": "12.2", "version": "5.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v5.1.zip", "size": "25.2 KB" }, { "serverVersion": "13.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v6.zip", "size": "25.2 KB" }, { "serverVersion": "13.1.1", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v6.0.1.zip", "size": "25.2 KB" }, { "serverVersion": "13.4.1", "version": "6.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v6.1.zip", "size": "28.3 KB" }, { "serverVersion": "13.4.2", "version": "6.2", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v6.2.zip", "size": "28.4 KB" }, { "serverVersion": "14.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v7.zip", "size": "28.7 KB" }, { "serverVersion": "14.3", "version": "7.1", "url": "https://download.technitium.com/dns/apps/BlockPageApp-v7.1.zip", "size": "28.88 KB" } ] }, { "name": "Default Records", "description": "Allows setting one or more default records for configured local zones.", "versions": [ { "serverVersion": "11.5", "version": "1.0", "url": "https://download.technitium.com/dns/apps/DefaultRecordsApp-v1.zip", "size": "13.3 KB" }, { "serverVersion": "12.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/DefaultRecordsApp-v2.zip", "size": "14.0 KB" }, { "serverVersion": "13.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/DefaultRecordsApp-v3.zip", "size": "13.8 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/DefaultRecordsApp-v4.zip", "size": "14.0 KB" } ] }, { "name": "DNS64", "description": "Enables DNS64 function for both authoritative and recursive resolver responses for use by IPv6 only clients.\n\nWarning! Installing DNS64 app without having NAT64 in place will cause connectivity issues for some websites.", "versions": [ { "serverVersion": "10.0", "version": "1.0", "url": "https://download.technitium.com/dns/apps/Dns64App-v1.zip", "size": "15.8 KB" }, { "serverVersion": "10.0.1", "version": "1.0.1", "url": "https://download.technitium.com/dns/apps/Dns64App-v1.0.1.zip", "size": "15.8 KB" }, { "serverVersion": "11.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/Dns64App-v2.zip", "size": "14.4 KB" }, { "serverVersion": "12.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/Dns64App-v3.zip", "size": "15.3 KB" }, { "serverVersion": "13.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/Dns64App-v4.zip", "size": "15.5 KB" }, { "serverVersion": "13.5", "version": "4.1", "url": "https://download.technitium.com/dns/apps/Dns64App-v4.1.zip", "size": "15.6 KB" }, { "serverVersion": "14.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/Dns64App-v5.zip", "size": "15.7 KB" } ] }, { "name": "DNS Block List (DNSBL)", "description": "Allows creating APP records in primary and forwarder zones that can return A or TXT records based on the DNS Block Lists (DNSBL) configured. The implementation is based on RFC 5782.", "versions": [ { "serverVersion": "11.0", "version": "1.0", "url": "https://download.technitium.com/dns/apps/DnsBlockListApp-v1.zip", "size": "18.5 KB" }, { "serverVersion": "11.0.3", "version": "1.0.1", "url": "https://download.technitium.com/dns/apps/DnsBlockListApp-v1.0.1.zip", "size": "18.7 KB" }, { "serverVersion": "12.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/DnsBlockListApp-v2.zip", "size": "19.5 KB" }, { "serverVersion": "13.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/DnsBlockListApp-v3.zip", "size": "19.4 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/DnsBlockListApp-v4.zip", "size": "19.7 KB" } ] }, { "name": "DNS Rebinding Protection", "description": "Protects from DNS rebinding attacks using configured private domains and networks.\n\nWarning! The app will remove private IP addresses from response for domain names not hosted locally.", "versions": [ { "serverVersion": "12.0", "version": "1.1", "url": "https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v1.1.zip", "size": "11.7 KB" }, { "serverVersion": "13.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v2.zip", "size": "11.8 KB" }, { "serverVersion": "13.1.1", "version": "3.0", "url": "https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v3.zip", "size": "12.9 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v4.zip", "size": "13.0 KB" } ] }, { "name": "Drop Requests", "description": "Drops incoming DNS requests that match list of blocked networks or blocked questions.", "versions": [ { "serverVersion": "9.0", "version": "2.1", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v2.1.zip", "size": "13.1 KB" }, { "serverVersion": "10.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v3.zip", "size": "13.6 KB" }, { "serverVersion": "10.0.1", "version": "3.0.1", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v3.0.1.zip", "size": "13.6 KB" }, { "serverVersion": "11.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v4.zip", "size": "12.1 KB" }, { "serverVersion": "12.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v5.zip", "size": "12.7 KB" }, { "serverVersion": "13.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v6.zip", "size": "12.6 KB" }, { "serverVersion": "13.4", "version": "6.1", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v6.1.zip", "size": "12.7 KB" }, { "serverVersion": "14.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/DropRequestsApp-v7.zip", "size": "12.7 KB" } ] }, { "name": "Failover", "description": "Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record based on the health status of the servers. The app supports email alerts and web hooks to relay the health status.", "versions": [ { "serverVersion": "9.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/FailoverApp-v4.zip", "size": "52.0 KB" }, { "serverVersion": "10.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/FailoverApp-v5.zip", "size": "53.2 KB" }, { "serverVersion": "10.0.1", "version": "5.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v5.1.zip", "size": "53.3 KB" }, { "serverVersion": "11.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/FailoverApp-v6.zip", "size": "47.5 KB" }, { "serverVersion": "11.0.3", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v6.0.1.zip", "size": "47.8 KB" }, { "serverVersion": "11.1", "version": "6.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v6.1.zip", "size": "47.9 KB" }, { "serverVersion": "11.2", "version": "6.1.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v6.1.1.zip", "size": "48.0 KB" }, { "serverVersion": "11.3", "version": "6.1.3", "url": "https://download.technitium.com/dns/apps/FailoverApp-v6.1.3.zip", "size": "48.0 KB" }, { "serverVersion": "11.5", "version": "6.2", "url": "https://download.technitium.com/dns/apps/FailoverApp-v6.2.zip", "size": "48.2 KB" }, { "serverVersion": "12.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/FailoverApp-v7.zip", "size": "49.7 KB" }, { "serverVersion": "12.1", "version": "7.0.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v7.0.1.zip", "size": "49.8 KB" }, { "serverVersion": "13.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/FailoverApp-v8.zip", "size": "50.0 KB" }, { "serverVersion": "13.2.1", "version": "8.0.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v8.0.1.zip", "size": "50.0 KB" }, { "serverVersion": "14.0", "version": "9.0", "url": "https://download.technitium.com/dns/apps/FailoverApp-v9.zip", "size": "50.5 KB" }, { "serverVersion": "14.3", "version": "9.1", "url": "https://download.technitium.com/dns/apps/FailoverApp-v9.1.zip", "size": "50.54 KB" } ] }, { "name": "Filter AAAA", "description": "Allows filtering AAAA records by returning NO DATA response when A records for the same domain name are available. This allows clients with dual-stack (IPv4 and IPv6) Internet connection to prefer using IPv4 to connect to websites and use IPv6 only when a website has no IPv4 support.", "versions": [ { "serverVersion": "12.2", "version": "1.0", "url": "https://download.technitium.com/dns/apps/FilterAaaaApp-v1.zip", "size": "12.9 KB" }, { "serverVersion": "13.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/FilterAaaaApp-v2.zip", "size": "12.9 KB" }, { "serverVersion": "13.1", "version": "3.0", "url": "https://download.technitium.com/dns/apps/FilterAaaaApp-v3.zip", "size": "13.8 KB" }, { "serverVersion": "13.1.1", "version": "3.1", "url": "https://download.technitium.com/dns/apps/FilterAaaaApp-v3.1.zip", "size": "14.1 KB" }, { "serverVersion": "13.2", "version": "3.2", "url": "https://download.technitium.com/dns/apps/FilterAaaaApp-v3.2.zip", "size": "14.1 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/FilterAaaaApp-v4.zip", "size": "14.3 KB" } ] }, { "name": "Geo Continent", "description": "Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record based on the continent the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method.", "versions": [ { "serverVersion": "9.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v4.zip", "size": "3.00 MB" }, { "serverVersion": "10.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v5.zip", "size": "2.76 MB" }, { "serverVersion": "10.0.1", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v5.0.1.zip", "size": "2.76 MB" }, { "serverVersion": "11.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v6.zip", "size": "2.76 MB" }, { "serverVersion": "11.0.3", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v6.0.1.zip", "size": "2.92 MB" }, { "serverVersion": "11.2", "version": "6.0.2", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v6.0.2.zip", "size": "2.92 MB" }, { "serverVersion": "11.3", "version": "6.0.4", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v6.0.4.zip", "size": "2.92 MB" }, { "serverVersion": "12.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v7.zip", "size": "3.16 MB" }, { "serverVersion": "12.1", "version": "7.1", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v7.1.zip", "size": "7.57 MB" }, { "serverVersion": "13.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v8.zip", "size": "8.35 MB" }, { "serverVersion": "13.6", "version": "8.1", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v8.1.zip", "size": "8.35 MB" }, { "serverVersion": "14.0", "version": "9.0", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v9.zip", "size": "10.5 MB" }, { "serverVersion": "14.3", "version": "9.0.1", "url": "https://download.technitium.com/dns/apps/GeoContinentApp-v9.0.1.zip", "size": "10.59 MB" } ] }, { "name": "Geo Country", "description": "Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record based on the country the client queries from using MaxMind GeoIP2 Country database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-Country.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method.", "versions": [ { "serverVersion": "9.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v4.zip", "size": "3.00 MB" }, { "serverVersion": "10.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v5.zip", "size": "2.76 MB" }, { "serverVersion": "10.0.1", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v5.0.1.zip", "size": "2.76 MB" }, { "serverVersion": "11.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v6.zip", "size": "2.76 MB" }, { "serverVersion": "11.0.3", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v6.0.1.zip", "size": "2.92 MB" }, { "serverVersion": "11.2", "version": "6.0.2", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v6.0.2.zip", "size": "2.92 MB" }, { "serverVersion": "11.3", "version": "6.0.4", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v6.0.4.zip", "size": "2.92 MB" }, { "serverVersion": "12.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v7.zip", "size": "3.16 MB" }, { "serverVersion": "12.1", "version": "7.1", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v7.1.zip", "size": "7.57 MB" }, { "serverVersion": "13.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v8.zip", "size": "8.35 MB" }, { "serverVersion": "13.6", "version": "8.1", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v8.1.zip", "size": "8.35 MB" }, { "serverVersion": "14.0", "version": "9.0", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v9.zip", "size": "10.5 MB" }, { "serverVersion": "14.3", "version": "9.0.1", "url": "https://download.technitium.com/dns/apps/GeoCountryApp-v9.0.1.zip", "size": "10.59 MB" } ] }, { "name": "Geo Distance", "description": "Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record of the server located geographically closest to the client using MaxMind GeoIP2 City database. Supports EDNS Client Subnet (ECS). This app requires MaxMind GeoIP2 database and includes the GeoLite2 version for trial. \n\nTo update the MaxMind GeoIP2 database for your app, download the GeoIP2-City.mmdb file from MaxMind and zip it. Use the zip file with the manual Update option. The app optionally also uses MaxMind ISP/ASN database which can be updated the with same method.", "versions": [ { "serverVersion": "9.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v4.zip", "size": "32.7 MB" }, { "serverVersion": "10.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v5.zip", "size": "32.5 MB" }, { "serverVersion": "10.0.1", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v5.0.1.zip", "size": "32.5 MB" }, { "serverVersion": "11.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v6.zip", "size": "32.5 MB" }, { "serverVersion": "11.0.3", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v6.0.1.zip", "size": "33.9 MB" }, { "serverVersion": "11.2", "version": "6.0.2", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v6.0.2.zip", "size": "33.9 MB" }, { "serverVersion": "11.3", "version": "6.0.4", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v6.0.4.zip", "size": "33.9 MB" }, { "serverVersion": "12.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v7.zip", "size": "31.0 MB" }, { "serverVersion": "12.1", "version": "7.1", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v7.1.zip", "size": "35.4 MB" }, { "serverVersion": "13.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v8.zip", "size": "33.4 MB" }, { "serverVersion": "14.0", "version": "9.0", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v9.zip", "size": "35.5 MB" }, { "serverVersion": "14.3", "version": "9.0.1", "url": "https://download.technitium.com/dns/apps/GeoDistanceApp-v9.0.1.zip", "size": "35.59 MB" } ] }, { "name": "Log Exporter", "description": "Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols).", "versions": [ { "serverVersion": "13.4", "version": "1.0", "url": "https://download.technitium.com/dns/apps/LogExporterApp-v1.zip", "size": "202 KB" }, { "serverVersion": "13.5", "version": "1.0.1", "url": "https://download.technitium.com/dns/apps/LogExporterApp-v1.0.1.zip", "size": "202 KB" }, { "serverVersion": "13.6", "version": "1.0.2", "url": "https://download.technitium.com/dns/apps/LogExporterApp-v1.0.2.zip", "size": "202 KB" }, { "serverVersion": "14.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/LogExporterApp-v2.zip", "size": "212 KB" }, { "serverVersion": "14.2.0", "version": "2.1", "url": "https://download.technitium.com/dns/apps/LogExporterApp-v2.1.zip", "size": "213.95 KB" } ] }, { "name": "MISP Connector", "description": "Block malicious domain names pulled from MISP feeds.\n\nNote! This app works independent of the DNS server's built-in blocking feature. The options configured in DNS server Settings section does not apply to this app.", "versions": [ { "serverVersion": "14.2.0", "version": "1.0", "url": "https://download.technitium.com/dns/apps/MispConnectorApp-v1.zip", "size": "24.49 KB" } ] }, { "name": "NO DATA", "description": "Allows creating APP records in Conditional Forwarder zones to return NO DATA response for requests that match the configured query type (QTYPE) to prevent them from being forwarded.", "versions": [ { "serverVersion": "10.0", "version": "1.0", "url": "https://download.technitium.com/dns/apps/NoDataApp-v1.zip", "size": "11.0 KB" }, { "serverVersion": "10.0.1", "version": "1.0.1", "url": "https://download.technitium.com/dns/apps/NoDataApp-v1.0.1.zip", "size": "11.0 KB" }, { "serverVersion": "11.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/NoDataApp-v2.zip", "size": "10.1 KB" }, { "serverVersion": "11.0.3", "version": "2.0.1", "url": "https://download.technitium.com/dns/apps/NoDataApp-v2.0.1.zip", "size": "10.1 KB" }, { "serverVersion": "11.3", "version": "2.0.3", "url": "https://download.technitium.com/dns/apps/NoDataApp-v2.0.3.zip", "size": "10.1 KB" }, { "serverVersion": "12.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/NoDataApp-v3.zip", "size": "10.9 KB" }, { "serverVersion": "13.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/NoDataApp-v4.zip", "size": "10.7 KB" }, { "serverVersion": "14.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/NoDataApp-v5.zip", "size": "10.8 KB" } ] }, { "name": "NX Domain", "description": "Blocks configured domain names with a NX Domain response.", "versions": [ { "serverVersion": "9.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v2.zip", "size": "11.4 KB" }, { "serverVersion": "10.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v3.zip", "size": "12.0 KB" }, { "serverVersion": "10.0.1", "version": "3.0.1", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v3.0.1.zip", "size": "12.0 KB" }, { "serverVersion": "11.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v4.zip", "size": "10.7 KB" }, { "serverVersion": "12.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v5.zip", "size": "11.5 KB" }, { "serverVersion": "12.2", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v5.0.1.zip", "size": "11.4 KB" }, { "serverVersion": "13.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v6.zip", "size": "11.4 KB" }, { "serverVersion": "13.5", "version": "6.1", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v6.1.zip", "size": "11.6 KB" }, { "serverVersion": "14.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/NxDomainApp-v7.zip", "size": "13.6 KB" } ] }, { "name": "NX Domain Override", "description": "Overrides NX Domain response with custom A/AAAA record response for configured domain names.", "versions": [ { "serverVersion": "12.0", "version": "1.0", "url": "https://download.technitium.com/dns/apps/NxDomainOverrideApp-v1.zip", "size": "13.0 KB" }, { "serverVersion": "13.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/NxDomainOverrideApp-v2.zip", "size": "12.9 KB" }, { "serverVersion": "14.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/NxDomainOverrideApp-v3.zip", "size": "13.0 KB" } ] }, { "name": "Query Logs (MySQL)", "description": "Logs all incoming DNS requests and their responses in a MySQL/MariaDB database that can be queried from the DNS Server web console.\n\nNote! You will need to create a user and grant all privileges on the database to the user so that the app will be able to access it. To do that run the following commands with the required database name and username on your mysql root prompt:\nCREATE USER 'user'@'%' IDENTIFIED BY 'password';\nGRANT ALL PRIVILEGES ON DatabaseName.* TO 'user'@'%';\n\nOnce the database is configured, edit the app's config to update the database name, connection string, and set enableLogging to true. The app will automatically create the required database schema for you and start logging queries once you save the config.", "versions": [ { "serverVersion": "13.4", "version": "1.0", "url": "https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v1.zip", "size": "6.70 MB" }, { "serverVersion": "13.4.1", "version": "1.1", "url": "https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v1.1.zip", "size": "6.70 MB" }, { "serverVersion": "13.4.2", "version": "2.0", "url": "https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v2.zip", "size": "472 KB" }, { "serverVersion": "13.5", "version": "2.0.1", "url": "https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v2.0.1.zip", "size": "472 KB" }, { "serverVersion": "14.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v3.zip", "size": "478 KB" }, { "serverVersion": "14.3", "version": "3.0.1", "url": "https://download.technitium.com/dns/apps/QueryLogsMySqlApp-v3.0.1.zip", "size": "482.76 KB" } ] }, { "name": "Query Logs (Sqlite)", "description": "Logs all incoming DNS requests and their responses in a Sqlite database that can be queried from the DNS Server web console. The query logging throughput is limited by the disk throughput on which the Sqlite db file is stored. This app is not recommended to be used with very high throughput (more than 20,000 requests/second).", "versions": [ { "serverVersion": "9.0", "version": "2.0.4", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v2.0.4.zip", "size": "9.39 MB" }, { "serverVersion": "10.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v3.zip", "size": "13.0 MB" }, { "serverVersion": "10.0.1", "version": "3.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v3.1.zip", "size": "13.0 MB" }, { "serverVersion": "11.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.zip", "size": "13.7 MB" }, { "serverVersion": "11.1", "version": "4.0.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.0.1.zip", "size": "13.6 MB" }, { "serverVersion": "11.3", "version": "4.0.2", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.0.2.zip", "size": "13.6 MB" }, { "serverVersion": "11.5.2", "version": "4.0.3", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.0.3.zip", "size": "13.6 MB" }, { "serverVersion": "11.5.3", "version": "4.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v4.1.zip", "size": "13.7 MB" }, { "serverVersion": "12.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v5.zip", "size": "12.2 MB" }, { "serverVersion": "12.1", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v5.0.1.zip", "size": "12.2 MB" }, { "serverVersion": "12.2", "version": "5.0.2", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v5.0.2.zip", "size": "12.2 MB" }, { "serverVersion": "13.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v6.zip", "size": "12.2 MB" }, { "serverVersion": "13.3", "version": "6.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v6.1.zip", "size": "14.3 MB" }, { "serverVersion": "13.4", "version": "7.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v7.zip", "size": "14.3 MB" }, { "serverVersion": "13.6", "version": "7.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v7.1.zip", "size": "14.3 MB" }, { "serverVersion": "14.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v8.zip", "size": "14.6 MB" } ] }, { "name": "Query Logs (SQL Server)", "description": "Logs all incoming DNS requests and their responses in a Microsoft SQL Server database that can be queried from the DNS Server web console.", "versions": [ { "serverVersion": "13.4", "version": "1.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.zip", "size": "4.67 MB" }, { "serverVersion": "13.4.1", "version": "1.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.1.zip", "size": "4.67 MB" }, { "serverVersion": "13.4.2", "version": "1.2", "url": "https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.2.zip", "size": "4.67 MB" }, { "serverVersion": "13.5", "version": "1.2.1", "url": "https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.2.1.zip", "size": "4.67 MB" }, { "serverVersion": "13.6", "version": "1.2.2", "url": "https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v1.2.2.zip", "size": "4.67 MB" }, { "serverVersion": "14.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/QueryLogsSqlServerApp-v2.zip", "size": "4.82 MB" } ] }, { "name": "Split Horizon", "description": "Allows creating APP records in primary and forwarder zones that can return different set of A or AAAA records, or CNAME record for clients querying over public, private, or other specified networks.\n\nEnables Address Translation of IP addresses in a DNS response for A & AAAA type request based on the client's network address or configured domain names, and the configured 1:1 translation.", "versions": [ { "serverVersion": "9.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v4.zip", "size": "14.3 KB" }, { "serverVersion": "10.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v5.zip", "size": "19.5 KB" }, { "serverVersion": "10.0.1", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v5.0.1.zip", "size": "19.5 KB" }, { "serverVersion": "11.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v6.zip", "size": "17.6 KB" }, { "serverVersion": "11.0.3", "version": "6.0.1", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v6.0.1.zip", "size": "17.7 KB" }, { "serverVersion": "11.2", "version": "6.0.2", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v6.0.2.zip", "size": "17.7 KB" }, { "serverVersion": "11.3", "version": "6.0.4", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v6.0.4.zip", "size": "17.7 KB" }, { "serverVersion": "11.5", "version": "6.1", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v6.1.zip", "size": "18.6 KB" }, { "serverVersion": "12.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v7.zip", "size": "19.5 KB" }, { "serverVersion": "13.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v8.zip", "size": "19.4 KB" }, { "serverVersion": "13.5", "version": "8.1", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v8.1.zip", "size": "19.5 KB" }, { "serverVersion": "14.0", "version": "9.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v9.zip", "size": "19.7 KB" }, { "serverVersion": "14.3", "version": "10.0", "url": "https://download.technitium.com/dns/apps/SplitHorizonApp-v10.zip", "size": "20.27 KB" } ] }, { "name": "Weighted Round Robin", "description": "Allows creating APP records in primary and forwarder zones that can return A or AAAA records, or CNAME record using weighted round-robin load balancing.", "versions": [ { "serverVersion": "11.2", "version": "1.0", "url": "https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v1.zip", "size": "11.9 KB" }, { "serverVersion": "11.3", "version": "1.0.2", "url": "https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v1.0.2.zip", "size": "12.0 KB" }, { "serverVersion": "12.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v2.zip", "size": "12.7 KB" }, { "serverVersion": "13.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v3.zip", "size": "12.6 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/WeightedRoundRobinApp-v4.zip", "size": "12.7 KB" } ] }, { "name": "What Is My Dns", "description": "Allows creating APP records in primary and forwarder zones that can return the IP address of the user's DNS Server for A, AAAA, and TXT queries.", "versions": [ { "serverVersion": "9.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v4.zip", "size": "9.26 KB" }, { "serverVersion": "10.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.zip", "size": "9.93 KB" }, { "serverVersion": "11.0", "version": "5.0.1", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.1.zip", "size": "9.77 KB" }, { "serverVersion": "11.0.3", "version": "5.0.2", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.2.zip", "size": "9.79 KB" }, { "serverVersion": "11.2", "version": "5.0.3", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.3.zip", "size": "9.87 KB" }, { "serverVersion": "11.3", "version": "5.0.5", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v5.0.5.zip", "size": "9.90 KB" }, { "serverVersion": "12.0", "version": "6.0", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v6.zip", "size": "10.6 KB" }, { "serverVersion": "13.0", "version": "7.0", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v7.zip", "size": "10.5 KB" }, { "serverVersion": "14.0", "version": "8.0", "url": "https://download.technitium.com/dns/apps/WhatIsMyDnsApp-v8.zip", "size": "10.5 KB" } ] }, { "name": "Wild IP", "description": "Allows creating APP records in primary and forwarder zones that can return the IP address embedded in the subdomain name for A and AAAA queries. It works similar to sslip.io.", "versions": [ { "serverVersion": "9.0", "version": "1.0", "url": "https://download.technitium.com/dns/apps/WildIpApp-v1.zip", "size": "9.66 KB" }, { "serverVersion": "10.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/WildIpApp-v2.zip", "size": "10.3 KB" }, { "serverVersion": "11.0", "version": "2.1", "url": "https://download.technitium.com/dns/apps/WildIpApp-v2.1.zip", "size": "11.0 KB" }, { "serverVersion": "11.0.3", "version": "2.1.1", "url": "https://download.technitium.com/dns/apps/WildIpApp-v2.1.1.zip", "size": "11.0 KB" }, { "serverVersion": "11.2", "version": "2.2", "url": "https://download.technitium.com/dns/apps/WildIpApp-v2.2.zip", "size": "11.2 KB" }, { "serverVersion": "12.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/WildIpApp-v3.zip", "size": "12.2 KB" }, { "serverVersion": "13.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/WildIpApp-v4.zip", "size": "12.1 KB" }, { "serverVersion": "14.0", "version": "5.0", "url": "https://download.technitium.com/dns/apps/WildIpApp-v5.zip", "size": "12.2 KB" } ] }, { "name": "Zone Alias", "description": "Allows configuring aliases for any zone (internal or external) such that they all return the same set of records.", "versions": [ { "serverVersion": "11.3", "version": "1.0", "url": "https://download.technitium.com/dns/apps/ZoneAliasApp-v1.zip", "size": "12.2 KB" }, { "serverVersion": "12.0", "version": "2.0", "url": "https://download.technitium.com/dns/apps/ZoneAliasApp-v2.zip", "size": "13.0 KB" }, { "serverVersion": "13.0", "version": "3.0", "url": "https://download.technitium.com/dns/apps/ZoneAliasApp-v3.zip", "size": "12.9 KB" }, { "serverVersion": "13.5", "version": "3.1", "url": "https://download.technitium.com/dns/apps/ZoneAliasApp-v3.1.zip", "size": "13.0 KB" }, { "serverVersion": "14.0", "version": "4.0", "url": "https://download.technitium.com/dns/apps/ZoneAliasApp-v4.zip", "size": "13.2 KB" } ] } ] ================================================ FILE: CHANGELOG.md ================================================ # Technitium DNS Server Change Log ## Version 14.3 Release Date: 20 December 2025 - Added support for Dark Mode. Thanks to @skidoodle for the PR. - Updated Catalog zones implementation to allow adding Secondary zones as members. - Updated Restore Settings option to allow importing backup zip files from older DNS server versions. - Added new options in Settings to configure default TTL values for NS and SOA records. - Added DNS record overwrite option in DHCP Scopes to allow dynamic leases to overwrite any existing DNS A record for the client domain name. - Advanced Blocking App: Added new option to allow configuring block list update interval in minutes. - Split Horizon App: Updated app to support mapping domain names to group for address translation feature. - Multiple other minor bug fixes and improvements. ## Version 14.2 Release Date: 22 November 2025 - Fixed bug in Clustering implementation which prevented using IPv4 and IPv6 addresses together. Thanks to @ruifung for the PR. - There is also a breaking change in clustering and thus all cluster nodes must be upgraded to this release to avoid issues. - Updated the "Allow / Block List URLs" option implementation to support comment entries. - Advanced Blocking App: Updated app to implement `blockingAnswerTtl` option to allow specifying the TTL value used in blocked response. - Log Exporter App: Updated the app to add EDNS logging support. Thanks to @zbalkan for the PR. - MISP Connector App: Added new app that can block malicious domain names pulled from MISP feeds. Thanks to @zbalkan for the PR. - Multiple other minor bug fixes and improvements. ## Version 14.1 Release Date: 16 November 2025 - Updated Clustering implementation to allow configuring multiple custom IP addresses. This introduces a breaking change in the API and thus all cluster nodes must be upgraded to this release for them to work together. - Fixed issues related to user and group permission validation when Clustering is enabled which caused permission bypass when accessing another node. - Fixed bug that caused the Advanced Blocking app to stop working. - Added environment variables for TLS certificate path, certificate password, and HTTP to HTTPS redirect option. Thanks to @simonvandermeer for the PR. - Updated Hagezi block list URLs. Thanks to @hagezi for the PR. - Other minor changes and improvements. ## Version 14.0.1 Release Date: 9 November 2025 - Fixed bugs in the Force Update Block List and Temporary Disable Blocking API calls. - Fixed session validation bypass bug during proxying request to another node when Clustering is enabled. - Fixed issue of failing to load app config due to text encoding issues. - Fixed issue of failure to load old config file versions due to validation failures in some cases. - Updated GUI docs for Cluster initialization and joining. - Other minor changes and improvements. ## Version 14.0 Release Date: 8 November 2025 - Upgraded codebase to use .NET 9 runtime. If you had manually installed the DNS Server or .NET 8 Runtime earlier then you must install .NET 9 Runtime manually before upgrading the DNS server. - This major release has a breaking changes in the Change Password HTTP API so its advised to test your API client once before deploying to production. - Fixed Denial of Service (DoS) vulnerability in the DNS server's rate limiting implementation reported by Shiming Liu from the Network and Information Security Lab, Tsinghua University. The DNS Server now has a redesigned rate limiting implementation with different Queries Per Minute (QPM) options in Settings that help mitigate this issue. - Fixed Cache Poisoning vulnerability achieved using a IP fragmentation attack reported by Yuxiao Wu from the NISL Lab Security, Tsinghua University. The DNS server fixes this issue by adding missing bailiwick validations for NS record in referral responses. - Fixed [DNSSEC Downgrade](https://dnssec-downgrade.net/) vulnerability that made it possible to bypass validation when one of domain name's DNSSEC algorithm was not supported by the DNS server. - Implemented Clustering feature where you can now create a cluster of two or more DNS server instances and manage all of them from a single DNS admin web console by logging into anyone of the Cluster nodes. It also features showing aggregate Dashboard data for the entire cluster. - Added TOTP based Two-factor authentication (2FA) support. - Added options to configure UDP Socket pooling feature in Settings. - Fixed bug in zone file parsing that failed to parse records when their names were not FDQN and matched with name of a record type. - Fixed issue with internal Http Client to retry for IPv4 addresses too when `Prefer IPv6` option is enabled and IPv6 address failed to connect. - Fixed bug of missing NSEC/NSEC3 record in response for wildcard and Empty Non-terminal (ENT) records in Primary zones. - Fixed multiple issues in Prefetch and Auto Prefetch implementation that caused undesirable frequent refreshing of cached data in certain cases. - Query Logs (Sqlite) App: Updated app to use Channels for better performance. - Query Logs (MySQL) App: Updated app to use Channels for better performance. Fixed bug in schema for protocol parameter causing overflow. - Query Logs (SQL Server) App: Updated app to use Channels for better performance. - NX Domain App: Updated app to support Extended DNS Error messages. - Multiple other minor bug fixes and improvements. ## Version 13.6 Release Date: 26 April 2025 - Added option to import a zone file when adding a Primary or Forwarder zone. This allows using a template zone file when creating new zones. - Updated the web GUI to support custom lists for DNS Client server list, quick block drop down list and quick forwarders drop down list. To create a customized list, read the instructions given in the `www/json/readme.txt` file found in the installation folder. - Updated the record filtering option in zone edit view to support wildcard based search. - Fixed issue in DNS-over-QUIC service that caused the service to stop working due to failed connection handshake. - Query Logs (Sqlite) App: Updated app to support VACCUM option to allow trimming database file on disk to reduce its size. - Geo Continent App and Geo Country App: Updated both apps to support macro variable to simplify APP record data JSON configuration. - Multiple other minor bug fixes and improvements. ## Version 13.5 Release Date: 6 April 2025 - Implemented [RFC 8080](https://datatracker.ietf.org/doc/rfc8080/) to add support for Ed25519 (15) and Ed448 (16) DNSSEC algorithms for both signing and validation. - Added support for user specified DNSSEC private keys. This adds option to specify private key in PEM format when signing zone or when doing a key rollover. - Added feature to filter records in the zone editor based on its name or type to allow ease of searching records in large zones. - Added support for writing DNS logs to Console (STDOUT) along with existing option to write to a file. - Updated Import Zone option to allow importing directly from a given file along with existing option to enter records to import with a text editor. - Updated zone file parser to support BIND extended zone file format. - Updated Query Logs view to show records with background color based on the type of log entry. - Implemented [draft-fujiwara-dnsop-resolver-update](https://datatracker.ietf.org/doc/draft-fujiwara-dnsop-resolver-update/) to cache parent side NS records and child side authoritative NS records separately in DNS cache. - Removed [NS Revalidation (draft-ietf-dnsop-ns-revalidation)](https://datatracker.ietf.org/doc/draft-ietf-dnsop-ns-revalidation/) feature implementation. This featured caused increase in complexity and number of requests to name servers increasing load on the resolver. It also caused few domain names to fail to resolve when the zone's child NS records were different from parent NS records which would have otherwise resolved correctly. It did not add any benefit for the resolver operator but created operational issues. Read the discussion thread [here](https://mailarchive.ietf.org/arch/msg/dnsop/s8KBhilK4bCrmSBRMyKaxll02lk/) to understand more about this decision. - Added `IDnsApplicationPreference` interface to allow applications to be ordered based on their user configured app preference value. - Advanced Forwarding App, DNS64 App, NXDOMAIN App, Split Horizon App and Zone Alias App: Updated these apps to implement app preference feature in config with new `appPreference` option. - Log Exporter App: Updated app to allow configuring HTTP headers without validation to allow adding non-standard header values. - Updated the DNS admin panel web app to use relative paths to allow using the DNS admin panel with any URL path on a reverse proxy. - Multiple other minor bug fixes and improvements. ## Version 13.4.3 Release Date: 23 February 2025 - Fixed issue of high memory usage when "Last Year" option is used on Dashboard. - Fixed multiple issues of DNSSEC validation failures for certain domain names when using forwarders. - Multiple other minor bug fixes and improvements. ## Version 13.4.2 Release Date: 15 February 2025 - Fixed issue of unhandled CD flag condition when DO flag is unset in requests for a specific case. - Block Page App: Fixed issue with Kestrel local addresses that caused failure to bind on Linux systems. - Query Logs (MySQL) App: Updated app to use MySqlConnector driver which allows the app to work with MariaDB too. - Query Logs (SQL Server) App: Fixed issue with bulk insert due to limit on parameters per query. Fixed issue with qtype filtering. - Multiple other minor bug fixes and improvements. ## Version 13.4.1 Release Date: 2 February 2025 - Fixed issue of unhandled CD flag condition when DO flag is unset in requests. - Block Page App: Updated app to show blocking info details on the block page. - Query Logs (MySQL) App: Updated app to add server domain to db logs to allow using same db with multiple instances. - Query Logs (SQL Server) App: Updated app to add server domain to db logs to allow using same db with multiple instances. - Multiple other minor bug fixes and improvements. ## Version 13.4 Release Date: 26 January 2025 - Added implementation to detect spoofed DNS responses over UDP transport and switch to TCP transport to mitigate cache poisoning attempts. This is a mitigation for RebirthDay Attack [CVE-2024-56089] reported by Xiang Li, AOSP Lab of Nankai University. - Added support for reading minute stats for given custom date time range (for max 2 hours range difference). - Added HTTP API and GUI option to export Query Logs as a CSV file. - Drop Requests App: Fixed bug that caused matching all requests when unknown record type was configured. - Log Exported App: Added new app that supports exporting query logs to file, HTTP, and Syslog sinks. The app was designed and implemented by [Zafer Balkan](https://github.com/zbalkan). - Query Logs (SQL Server) App: Added new app that supports logging query logs to Microsoft SQL Server. - Query Logs (MySQL) App: Added new app that supports logging query logs to MySQL database server. - Multiple other minor bug fixes and improvements. ## Version 13.3 Release Date: 21 December 2024 - Implemented resolver queue mechanism to avoid request timeout error issues caused when too many outbound resolutions were being processed concurrently for large deployments. A new Max Concurrent Resolutions option is now available in Settings > General section to configure the maximum number of concurrent async resolutions per CPU core. - Added new Minimum SOA Refresh and Minimum SOA Retry options in Settings > General section to override any Secondary, Stub, Secondary Forwarder, or Secondary Catalog zone SOA values that are smaller than these configured minimum values. - Added feature to include Subject Alternative Name (SAN) entry for DNS admin web service local unicast addresses in the self-signed certificate. - Fixed bug in NSEC3 non-existent proof generation implementation that caused Denial of Service (DoS) for all DNS protocol services when certain primary and secondary zones are DNSSEC signed using NSEC3. - Fixed issue of unhandled exception that caused Denial of Service (DoS) for DNS-over-QUIC service [CVE-2024-56946] reported by Michael Wedl, St. Poelten University of Applied Sciences. - Fixed bug in reloading SSL/TLS certificate for DNS admin web service and DNS-over-HTTPS service. - Fixed issue with Catalog zone SOA request that caused zone transfer to fail with BIND. - Query Logs (Sqlite): Updated the app to support logging response RTT value. - Multiple other minor bug fixes and improvements. ## Version 13.2.2 Release Date: 2 December 2024 - Fixed bug that caused DNS response to include bogus records even when Checking Disabled (CD) is set to false in request. ## Version 13.2.1 Release Date: 30 November 2024 - Updated server to allow DNS-over-HTTPS service to read X-Real-IP header from reverse proxy that are allowed by the ACL. - Fixed issue with HTTP/2 on OS versions older than Windows 10 that caused failure to enable HTTPS for admin web service and DNS-over-HTTPS service. - Fixed issue with handling connection abort condition for DNS-over-QUIC. - Fixed issue with handling a wildcard query case for ENT subdomain names in local zones. - Fixed issue with Forwarding where CNAME was not being resolved separately when upstream returned SOA in response authority section. - Fixed issue in DNS Application assembly loading implementation that caused issue loading dependencies for some scenarios. - Multiple other minor bug fixes and improvements. ## Version 13.2 Release Date: 16 November 2024 - Added new option in Settings to allow configuring reverse proxy network ACL to use with DNS-over-UDP-PROXY, DNS-over-TCP-PROXY, AND DNS-over-HTTP optional protocols. - Fixed issue in DNS-over-QUIC protocol client which caused the forwarding to fail to work with timeout error after a while in some cases. ## Version 13.1.1 Release Date: 9 November 2024 - Fixed issue with HTTP/3 protocol not working for both admin web service and DNS-over-HTTPS/3 service caused due to changes in how Kestrel web server uses application protocol option. - Updated DNS-over-HTTPS client implementation such that it will support HTTP/2 and HTTP/1.1 protocols with `https` scheme and only support HTTP/3 protocol with `h3` scheme with no protocol fallback. - Fixed issue in DNS-over-TCP and DNS-over-TLS client caused due to some platforms not supporting TCP keep alive socket options. - Updated recursive resolver implementation to always attempt to resolve AAAA for name server with missing IPv6 glue record to allow resolution over IPv6 only networks. - Filter AAAA App: added new option to configuring default TTL value. - DNS Rebinding Protection App: added new option to configure bypass networks. - Multiple other minor bug fixes and improvements. ## Version 13.1 Release Date: 19 October 2024 - Added new option to add Secondary Root Zone directly. - Added new notify option for Catalog zones to specify separate name servers only for Catalog zone updates. - Added option to configure blocking answer's TTL value in Settings. - Added option to make the `X-Real-IP` header customizable for admin web service and for DNS-over-HTTP optional protocol. - Multiple other minor bug fixes and improvements. - Filter AAAA App: updated app to support option to explicitly specify filter domain names. ## Version 13.0.2 Release Date: 28 September 2024 - Fixed issue with DNS-over-TLS and DNS-over-TCP protocols that would cause the underlying connection to close if original request gets canceled. - Multiple other minor bug fixes and improvements. ## Version 13.0.1 Release Date: 23 September 2024 - Fixed issue in using proxy with forwarders that caused failure to use DNS-over-TOR with Cloudflare's hidden service. ## Version 13.0 Release Date: 22 September 2024 - Implemented Catalog Zones [RFC 9432](https://datatracker.ietf.org/doc/rfc9432/) support to allow automatic DNS zone provisioning to one or more secondary name servers. The implementation supports Primary, Stub, and Conditional Forwarder zones for automatic provisioning of their respective secondary zones. - Added new Secondary Forwarder zone support to allow configuring secondaries for Conditional Forwarder zones. Conditional Forwarder zones now support zone transfer and notify features to support secondaries and will now contain a dummy SOA record. - Added Query Access feature to allow configuring access to each individual zone. This allows limiting query access to only clients on configured networks even when the DNS server is publicly accessible. - Added support for specifying Expiry TTL for records in zones that will cause the DNS server to automatically delete the records when Expiry TTL elapses. - Added support for concurrency in recursive resolver to allow querying more than one name server at a time to improve resolution performance. - Added support for latency based name server selection algorithm that works with concurrency feature for both recursive resolution and forwarders to significantly improve resolution performance. - Implemented priority support for Conditional Forwarder FWD records which can be used to prioritize some forwarders and have a low priority "This Server" FWD record to perform recursive resolution if needed. - Implemented ZONEMD [RFC 8976](https://datatracker.ietf.org/doc/rfc8976/) validation support for Secondary zones which is intended to be used with local secondary ROOT zone. This feature allows validating complete zone after each zone transfer. - Added support for Responsible Person (RP) record [RFC 1183](https://www.rfc-editor.org/rfc/rfc1183). - Added option to enable/disable Concurrent Forwarding feature so as to allow having sequential forwarding support. - The DNS Server now supports Network Access Control Lists for Recursion, Zone Transfer, and Dynamic Updates in both the GUI and HTTP API. - Changed the Unsupported NSEC3 Iteration Value implementation due to bug in previous implementation that caused failure to validate in some cases. - Improved brute force protection implementation for admin web service for IPv6 networks. - Added feature to write client subnet query rate limiting events to log file to allow tracking. - This major update has some breaking changes with SOA record and Zone Options related HTTP API calls. Some options in SOA record have been moved to Zone Options in both HTTP API and GUI. There are few breaking changes with the DNS Client library code too so any custom DNS App should be tested before upgrading the DNS server. - Multiple other minor bug fixes and improvements. ## Version 12.2.1 Release Date: 15 June 2024 - Fixed issue in DHCP server that caused failure to allocate lease due to hash code mismatch. - Fixed issue that may create empty zone files after the zone was deleted. ## Version 12.2 Release Date: 15 June 2024 - Added support for NAPTR record type. - Added Default Responsible Person option in Settings to use when adding Primary Zones. - Updated Serve Stale implementation to allow configuring Answer TTL, Reset TTL, and Max Wait Time options in Settings. - Updated SVCB/HTTPS record implementation to add support for automatic IP address hints. - Updated TXT record implementation to allow preserving the character-strings for a given TXT record to allow support for [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763). - Updated DNS Server's System Tray app on Windows with new context menu option to allow configuring Automatic Firewall entry feature. - Fixed issue with NSEC proof validation for wildcard empty non-terminal (ENT) cases. - Fixed issue with QNAME minimization implementation caused when NSEC3 unsupported iteration count event is encountered while resolving. - Added support for .p12 certificate file extension along with existing .pfx extension. - Filter AAAA App: Added new app that allows filtering AAAA records by returning NO DATA response when A records for the same domain name are available. This allows clients with dual-stack (IPv4 and IPv6) Internet connection to prefer using IPv4 to connect to websites and use IPv6 only when a website has no IPv4 support. - Query Logs (Sqlite) App: Fixed issue of failing to load the app on Alpine Linux. - Multiple other minor bug fixes and improvements. ## Version 12.1 Release Date: 16 March 2024 - Fixed [Key Trap](https://www.athene-center.de/en/keytrap) [vulnerability](https://www.athene-center.de/fileadmin/content/PDF/Technical_Report_KeyTrap.pdf) [CVE-2023-50387] that affected DNSSEC validation which can cause DoS affecting the DNS server's ability to resolve domain names. The mitigations will allow the DNS server to work even with high CPU usage. - The mitigation now allows max 4 DNSKEY records with key tag collision. - Limits cryptographic failures to max 16. - More that 8 RRSIG validation attempts per response will cause suspension of the task with max 16 suspensions allowed before the validation stops for the response. - Fixed vulnerability in NSEC3 closest encloser proof [CVE-2023-50868] that affected DNSSEC validation which can cause DoS affecting the DNS server's ability to resolve domain names. The mitigations will allow the DNS server to work even with high CPU usage. - More than 8 NSEC3 hash calculation per response will cause suspension of the task. - After 16 suspensions the the validation will stop for the response. - Fixed [Non-Responsive Delegation Attack](https://www.usenix.org/system/files/sec23fall-prepub-309-afek.pdf) (NRDelegation Attack) vulnerability [CVE-2022-3204]. - Fixed [NXNSAttack](https://arxiv.org/abs/2005.09107) vulnerability [CVE-2020-12662]. - Implemented NSEC3 iteration limit of 100. NSEC3 with iterations of more than 100 will be treated as No Proof. - Added EDNS Client Subnet (ECS) override feature to allow the DNS server to use the provided network subnet with ECS for all outbound requests. - Secondary zones now allow configuring Dynamic Updates permissions in Zone Options. - Import zone feature now supports option to overwrite SOA serial from SOA record being imported. - DNS Client now supports EDNS Client Subnet (ECS) option to allow testing ECS related issues with ease. - DNS cache entries now show request meta data to allow knowing the name server that provided the record data. - DHCP Scope now supports option to ignore Client Identifier option in requests to allow using the client's hardware address for lease management. - Advanced Blocking App: Updated implementation to support using domain names for local endpoint group map feature which will work with requests over DoT, DoH and DoQ protocols. - Advanced Forwarding App: Updated AdGuard upstream implementation to support multiple forwarders. - Geo Continent App: Updated app to support MaxMind ISP/ASN database to allow returning optimal ECS scope prefix in response. - Geo Country App: Updated app to support MaxMind ISP/ASN database to allow returning optimal ECS scope prefix in response. - Geo Distance App: Updated app to support MaxMind ISP/ASN database to allow returning optimal ECS scope prefix in response. - Fixed bug in authoritative zone wildcard matching. - Multiple other minor bug fixes and improvements. ## Version 12.0.1 Release Date: 8 February 2024 - Fixed bug in authoritative zone wildcard matching for empty non-terminal (ENT) records. - Fixed other minor issues. ## Version 12.0 Release Date: 4 February 2024 - Upgraded codebase to use .NET 8 runtime. If you had manually installed the DNS Server or .NET 7 Runtime earlier then you must install .NET 8 Runtime manually before upgrading the DNS server. - Fixed pulsing DoS vulnerability [CVE-2024-33655] reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) by updating the default configured values for the DNS server which mitigates the impact. - Added "Dropped" request stats on the Dashboard and main chart which shows the number of request that were dropped by the DNS server due to rate limiting or by the Drop Requests app. - Added transport protocol types chart on Dashboard which shows the protocol stats for the requests received by the DNS server. - Added feature to specify one or more source addresses for outbound DNS requests when the server is connected to two or more networks. - Added option to allow IP address or networks to allow accepting Notify requests from to avoid having to configure the same individually for each zone. - Added option to specify QPM bypass list to allow IP addresses or networks to bypass rate limiting restrictions. - Added feature to enable In-Memory stats such that only Last Hour data to be available on Dashboard and no stats data will be stored on disk. - Updated DNS-over-HTTPS implementation to work over SOCKS5 proxy when using HTTP/3 protocol (URL with `h3` scheme). - Added support for automatic initializing of DNS server root servers list with priming queries [RFC 8109](https://datatracker.ietf.org/doc/rfc8109/). - Conditional Forwarder Zones now support Dynamic Updates [RFC 2136](https://datatracker.ietf.org/doc/rfc2136/). - DNS Rebinding Protection App: A new app available that protects from DNS rebinding attacks using configured private domains and networks. - NX Domain Override App: New app to allow overriding NX Domain response to with custom A/AAAA record response for configured domain names. - Block Page App: Updated the app to use Kestrel web server and allow configuring multiple web servers that listen on different IP addresses. - Multiple other minor bug fixes and improvements. ## Version 11.5.3 Release Date: 7 November 2023 - Fixed bug in authoritative zone wildcard matching which caused NXDOMAIN response for some subdomain name requests. ## Version 11.5.2 Release Date: 31 October 2023 - Fixed bug in zone Dynamic Updates allowed IP/network addresses that caused failure to match with request IP address. ## Version 11.5.1 Release Date: 30 October 2023 - Fixed bug in validation code for DNS-over-TLS library that caused failure when trying to use the protocol. - Advanced Blocking App: Fixed minor issue in initializing the app. ## Version 11.5 Release Date: 29 October 2023 - Added support to import and export zones in standard RFC 1035 text file format. - Added feature to clone an existing zone with all its records and zone options. - Added DS Info viewer that shows all the info needed for updating DS records for the signed primary zone in a single view. - Added option to configure IP/network addresses that are allowed to perform zone transfer for all local zones without any TSIG authentication. - Added option to configure IP/network addresses that are allowed to bypass domain name blocking. - Added option to independently configure HTTP/3 protocol for DNS web service. - Added option to ignore resolver error logs so as to limit the log file size. - Added zone last modified date time stamp. - Added check for DNS web service local end point changes to ensure that the new end points are available to bind before saving settings to avoid locking out of the DNS admin web panel. - Updated DNS web service to revert to old local end point if new end point fails to bind. - Zone Options for zone transfer name servers and dynamic updates IP addresses can now accept network addresses too. - Updated conditional forwarder zones to allow bypassing default proxy configured in the DNS Server Settings. - Added new `IDnsRequestBlockingHandler` interface for DNS apps to allow the same level of blocking support as that of the DNS server's built-in blocking feature. - Advanced Blocking App: Updated app to implement the new `IDnsRequestBlockingHandler` interface. Added support to allow selecting group based on the DNS server local end point on which the request was received. - Split Horizon App: Address translation now supports using network addresses too for external to internal translation. - Default Records App: New app added that allows setting one or more default records for configured local zones. - Multiple other minor bug fixes and improvements. ## Version 11.4.1 Release Date: 13 August 2023 - Fixed issue that caused backup operations to fail. - Fixed minor issue with incremental zone transfer which caused empty nodes to not get removed from secondary zones. ## Version 11.4 Release Date: 12 August 2023 - Added support for DNS over [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) version 1 and 2 for both UDP and TCP transports. This feature allows using a load balancer or reverse proxy in front of the DNS server such that the client's IP address information is passed to the DNS server. This can also be used to provide DNS-over-TLS service with a TLS terminating reverse proxy that forwards request to TCP-PROXY protocol port. - Updated TLS certificate implementation to allow the TLS handshake to always send the certificate chain. - Updated Backup and Restore feature to include Web Service and Optional Protocols certificate files when they exist within the DNS server's config folder. - Added DNS server uptime info in the About section. - Multiple other minor bug fixes and improvements. ## Version 11.3 Release Date: 2 July 2023 - Added support for URI record type ([RFC 7553](https://www.rfc-editor.org/rfc/rfc7553.html)). - Added support for `dohpath` parameter for SVCB record type ([draft-ietf-add-svcb-dns](https://datatracker.ietf.org/doc/draft-ietf-add-svcb-dns/)). - Added support for configuring generic parameter for SVCB & HTTPS record types in UI. - Added feature to allow converting zone from one type to another to help scenarios like upgrade of a secondary zone to primary zone when decommissioning the existing primary zone. - Updated primary zone NOTIFY implementation to keep rechecking when notify fails and explicitly show notify failed status against the specific name servers in the UI. - Zone Alias App: Added new DNS app that allows creating aliases for any zone (internal or external) such that they all return the same set of records. - Multiple other minor bug fixes and improvements. ## Version 11.2 Release Date: 27 May 2023 - Added support for SVCB and HTTPS record types ([draft-ietf-dnsop-svcb-https](https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/)). - Added support for managing unknown (unsupported) record types. - Auto PTR App: Added new DNS app that can generate automatic responses for PTR requests. - Weighted Round Robin App: Added new app to allow returning responses with weighted round robin load balancing. - Multiple other minor bug fixes and improvements. ## Version 11.1.1 Release Date: 1 May 2023 - Fixed issue of UDP socket pool exhaustion on Windows platform causing all outbound UDP requests to fail. ## Version 11.1 Release Date: 29 April 2023 - Added support for Internationalized Domain Names (IDN). - Added support for primary zone's SOA record to have serial number date scheme. - Fixed issue reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) that made the DNS server vulnerable to cache poisoning on Windows platform due to non-random UDP ports for outbound requests. - Fixed bug in validation check during refreshing RRSIG records when primary zone is signed with NSEC3. - Fixed bug in NSEC3 record's types field which caused missing of RRSIG type entry. - Fixed issue to allow Kestrel web server to serve unknown file types to allow certbot webroot HTTP challenge to work as expected. - Advanced Forwarding App: Fixed the implementation to correctly store cached records per client subnet defined in the app's config. Added wildcard domain support. - Multiple other minor bug fixes and improvements. ## Version 11.0.3 Release Date: 11 March 2023 - Fixed DoS vulnerability reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) that an attacker can use to send bad-formatted UDP packet to cause the outbound requests to fail to resolve due to insufficient validation. - Fixed issue reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) that caused conditional forwarder to not honoring RD flag in requests. - Fixed issue reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) that made amplification attacks more effective due to max 4096 bytes limit for responses. - Fixed issue in loading of Allowed and Blocked zones that resulted in loading to take too much time caused due to indexing feature added in last update for authoritative zones. - Updated DNS server UDP response processing to remove glue records for MX responses and try again to send it instead of sending a truncated response that was causing issue with some old mail servers that did not perform follow up request over TCP. - Block Page App: Updated the app to support option to disable the web server without requiring to uninstall the app to stop the web server. - Multiple other minor bug fixes and improvements. ## Version 11.0.2 Release Date: 26 February 2023 - Fixed issue with DNS-over-HTTP private IP check that was causing 403 response when using with reverse proxy. - Fixed issue with zone record pagination caused when zone has no records. ## Version 11.0.1 Release Date: 25 February 2023 - Changed allow list implementation to handle them separately and show allow list count on Dashboard. - Fixed bug in conditional forwarder zone for root zone that caused the DNS server to return RCODE=ServerFailure. - Fixed issues with DNS server's App request query handling sequence to fix issues with Advanced Forwarding app. - Fixed issues with block list parser to detect in-line comments. - Fixed issue of "URI too long" in save DHCP scope action. - Updated Linux install script to use new install path in `/opt` and new config path `/etc/dns` for new installations. - Updated Docker container to use new volume path `/etc/dns` for config. - Updated Docker container to correctly handle container stop event to gracefully shutdown the DNS server. - Updated Docker container to include `libmsquic` to allow QUIC support. - Multiple other minor bug fixes and improvements. ## Version 11.0 Release Date: 18 February 2023 - Added support for DNS-over-QUIC (DoQ) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html). This allows you to run DoQ service as well as use it with Forwarders. DoQ implementation supports running over SOCKS5 proxy server that provides UDP transport. - Added support for Zone Transfer over QUIC (XFR-over-QUIC) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html). - Updated DNS-over-HTTPS protocol implementation to support HTTP/2 and HTTP/3. DNS-over-HTTP/3 can be forced by using `h3` instead of `https` scheme for the URL. - Updated DNS server's web service backend to use Kestrel web server and thus the DNS server now requires ASP.NET Core Runtime to be installed. With this change, the web service now supports both HTTP/2 and HTTP/3 protocols. If you are using HTTP API, it is recommended to test your code/script with the new release. - Added support to save DNS cache data to disk on server shutdown and to reload it at startup. - Updated DNS server domain name blocking feature to support Extended DNS Errors to show report on the blocked domain name. With this support added, the DNS Client tab on the web panel will show blocking report for any blocked domain name. - Updated DNS server domain name blocking feature to support wildcard block lists file format and Adblock Plus file format. - Updated DNS server to detect when an upstream server blocks a domain name to reflect it in dashboard stats and query logs. It will now detect blocking signal from Quad9 and show Extended DNS Error for it. - Updated web panel Zones GUI to support pagination. - Advanced Blocking App: Updated DNS app to support wildcard block lists file format. Updated the app to disable CNAME cloaking when a domain name is allowed in config. Implemented Extended DNS Errors support to show blocked domain report. - Advanced Forwarding App: Added new DNS app to support bulk conditional forwarder. - DNS Block List App: Added new DNS app to allow running your own DNSBL or RBL block lists [RFC 5782](https://www.rfc-editor.org/rfc/rfc5782). - Added support for TFTP Server Address DHCP option (150). - Added support for Generic DHCP option to allow configuring option currently not supported by the DHCP server. - Removed support for non-standard DNS-over-HTTPS (JSON) protocol. - Removed Newtonsoft.Json dependency from the DNS server and all DNS apps. - Multiple other minor bug fixes and improvements. ## Version 10.0.1 Release Date: 4 December 2022 - Fixed multiple issues in EDNS Client Subnet (ECS) implementation. - Fixed issue with serialization when saving permission data when there are more than 255 zones. - Failover App: Fixed issue with idle connection for HTTP/HTTPS probes. - QueryLogs (Sqlite) App: Fixes issue of open db file on windows installations. - Multiple other minor bug fixes and improvements. ## Version 10.0 Release Date: 26 November 2022 - Added Dynamic Updates [RFC 2136](https://www.rfc-editor.org/rfc/rfc2136) security policy support to allow updates only for specified domain names and record types. This adds breaking changes to the zone options HTTP API calls. Any implementation that uses the zone options API must test with new update before deploying to production. - Added support for DANE TLSA [RFC 6698](https://datatracker.ietf.org/doc/html/rfc6698) record type. This includes support for automatically generating the hash values using certificates in PEM format. - Added support for SSHFP [RFC 4255](https://www.rfc-editor.org/rfc/rfc4255.html) record type. - Implemented EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) support for recursive resolution and forwarding. - Updated HTTP API to accept date time in ISO 8601 format for dashboard and query logs API calls. Any implementation that uses these API must test with new update before deploying to production. - Upgraded codebase to .NET 7 runtime. If you had manually installed the DNS Server or .NET 6 Runtime earlier then you must install .NET 7 Runtime manually before upgrading the DNS server. - Fixed self-CNAME vulnerability [CVE-2022-48256] reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) which caused the DNS server to follow CNAME in loop causing the answer to contain couple of hundred records before the loop limit was hit. - Updated DNS Apps framework with `IDnsPostProcessor` interface to allow manipulating outbound responses by DNS apps. - NO DATA App: Added new app to allow returning NO DATA response in Conditional Forwarder zones to allow overriding existing records from the forwarder for specified record types. - DNS64 App: Added new app to support DNS64 function [RFC 6147](https://www.rfc-editor.org/rfc/rfc6147) for use by IPv6 only clients. - Advanced Blocking App: Upgraded the app code to use less memory when same block lists are used across multiple groups. - Geo Continent App, Geo Country App, and Geo Distance App: Upgraded the apps to support EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871). - Split Horizon App: Upgraded the app to add 1:1 IP address translation support. This allows mapping external/public IP address to internal/private IP address such that clients in private network can access local services using internal/private IP addresses. - Added support for Domain Search DHCP option [RFC 3397](https://www.rfc-editor.org/rfc/rfc3397) - Added support for CAPWAP Access Controller DHCP option [RFC 5417](https://www.rfc-editor.org/rfc/rfc5417.html). - Added DHCP Scope option to disable DNS updates. - Added DHCP Scope option to support domain name for NTP option such that the DHCP server will automatically resolve the domain names and use the resolved IP addresses with the NTP option. - Multiple other minor bug fixes and improvements. ## Version 9.1 Release Date: 9 October 2022 - Added Dynamic Updates [RFC 2136](https://www.rfc-editor.org/rfc/rfc2136) support. This allows using tools like `nsupdate`, allow 3rd party DHCP servers to update DNS records, and use certbot [certbot-dns-rfc2136](https://certbot-dns-rfc2136.readthedocs.io/en/stable/) plugin for automatic TLS certificate renewal using DNS challenge. - Updated dashboard to display main chart using client's local time instead of server's local time. - Fixed bug that caused error while adding new secondary zone. - Multiple other minor bug fixes and improvements. ## Version 9.0 Release Date: 24 September 2022 - Added multi-user role based access support. This allows creating multiple users and multiple role based groups with permission based access controls. - Added support for non-expiring API tokens to use with automation scripts. - Added zone level permissions support to allow access only to selected users or group members. - User profile options available to update each user's session timeout values. - HTTP API: The API has been updated extensively keeping backward compatibility. Any implementation that uses the API must test with new update before deploying to production. Using the non-expiring API tokens is recommended. - Updated Conditional Forwarder zones to support APP records to allow using DNS Apps in these zones. - Option added in Settings to stop block list URL automatic update. - DNS Apps: There is a breaking change in the IDnsAppRecordRequestHandler.ProcessRequestAsync() method. If you have any custom DNS app deployed, you need to recompile it with the latest DnsServerCore.ApplicationCommon.dll before updating to this new release. - DNS Apps now support automatic updates. The DNS server will check for updates and install them automatically every 24 hours. - Split Horizon App: Added feature to configure collection of networks to use with APP record data. - Wild IP App: Added new DNS App that returns a response A or AAAA queries with the IP address that is embedded in the subdomain name of the query. This app works similar to [sslip.io](https://sslip.io/). - Fixed minor issues in DNSSEC validation for DNAME responses and for wildcard NO DATA responses. - DHCP scopes now support updating DNS records in both Primary and Forwarder zones. - DHCP scopes now support blocking dynamic allocations to devices with locally administered MAC address. - Multiple other minor bug fixes and improvements. ## Version 8.1.4 Release Date: 3 July 2022 - Fixed issue in recursive resolution that caused DNSSEC validation to fail in cases when the name server responds with out-of-bailiwick records. - Updated recursive resolver to update addresses async for all NS records to improve performance. - Multiple other minor bug fixes and improvements. ## Version 8.1.3 Release Date: 11 June 2022 - Added OpenDNS DoH end points to DNS Client and Forwarder quick select list. - Fixed issue of missing digest type support check that could cause exception to be thrown causing failure to resolve the DNSSEC signed domain name. ## Version 8.1.2 Release Date: 28 May 2022 - Fixed issue in Primary zone add and update record IXFR history when RRSet TTL was updated. - Fixed issue in DNSSEC validation for MX and SRV records caused due to incorrect comparison of record data. - Fixed issue in SOA record responsible person parameter parsing. - This release updates delete and update record API calls for MX and SRV records which may cause issues in 3rd party clients if they are not updated before deploying this new version. It is recommended to check the API documentation for changes before deploying this new release. - Multiple other minor bug fixes and improvements. ## Version 8.1.1 Release Date: 21 May 2022 - Added Sync Failed and Notify Failed zone status to indicate issues between primary and secondary zones synchronization. - Added more options in zone options to configure zone transfer and notify settings. - Fixed DNSSEC signed primary zone key rollover timing issues as per [RFC 7583](https://datatracker.ietf.org/doc/html/rfc7583). - Fixed issue in recursive resolver by adding zone cut validation for glue records. - Multiple other minor bug fixes and improvements. ## Version 8.1 Release Date: 8 May 2022 - Fixed two ghost domain issues, CVE-2022-30257 (V1) and CVE-2022-30258 (V2), reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/). Issue V1 was fixed with some implementation changes in the NS Revalidation feature and thus having this option enabled in Settings will mitigate the issue. Issue V2 was fixed by implementing additional validation checks when caching NS records. - Added maximum cache entires option to limit memory usage by removing least recently used data from cache. - Implemented NS revalidation to revalidate parent side NS records when their TTL expires. - Updated the web console to store session token in local storage to prevent logging out on page reload. - DropRequests App: Added support to block entire zone for the configured QNAME. - Fixed bug in primary zone IXFR history caused due to missing SOA serial check. - Fixed issues with wrong IXFR history entries for DNSKEY records in primary zone. - Multiple other minor bug fixes and improvements. ## Version 8.0.2 Release Date: 3 April 2022 - Fixed bug in Conditional Forwarder zones that would cause ServerFailure responses for some queries. - Fixed issue of setting minimum TTL value to NSEC & NSEC3 records in Primary signed zones when SOA value is changed. - Fixed issue in parsing DNS-over-HTTPS JSON response for NSEC and NSEC3 records. - Multiple other minor bug fixes and improvements. ## Version 8.0.1 Release Date: 29 March 2022 - Fixed bug in Conditional Forwarder zones due to zone cut validation causing negative cache entry for CNAME responses which resulted in partial responses. - Fixed issue with handling FormatError response that were missing question section for EDNS requests. - Fixed minor issue with DNSSEC validation for unsigned zone when forwarder returns empty NXDOMAIN responses. - Fixed issue with NODATA response handling for ANAME records. - Fixed issue with record comment validation causing error when saving SOA records in zones. - Multiple other minor bug fixes and improvements. ## Version 8.0 Release Date: 26 March 2022 - Added EDNS support [RFC 6891](https://datatracker.ietf.org/doc/html/rfc6891). - Added Extended DNS Errors [RFC 8914](https://datatracker.ietf.org/doc/html/rfc8914). - Added DNSSEC validation support with RSA & ECDSA algorithms for recursive resolver, forwarders, and conditional forwarders. - Added DNSSEC support for all supported DNS transport protocols including encrypted DNS protocols (DoT, DoH, DoH JSON). - Added DNSSEC zone signing support with RSA & ECDSA algorithms. - Updated DNS Client to support DNSSEC validation. - Updated proprietary FWD record which is used with Conditional Forwarder Zones for DNSSEC validation and HTTP/SOCKS5 proxy support. - Updated Conditional Forwarder Zones to support working as a static stub zone to force a domain name to resolve via given name servers using NS records. - Upgraded codebase to .NET 6 runtime. - Query Logs App: Added wildcard search support for domain names. - Fixed multiple issues with DHCP server. - This release updates many API calls which may cause issues in 3rd party clients if they are not updated before deploying this new version. It is recommended to check the API documentation for changes before deploying this new release. - Multiple other minor bug fixes and improvements. ## Version 7.1 Release Date: 23 October 2021 - Added option in settings to automatically configure a self signed certificate for DNS web service. - Fixed cache poisoning vulnerability [CVE-2021-43105] reported by Xiang Li, [Network and Information Security Lab, Tsinghua University](https://netsec.ccert.edu.cn/) and Qifan Zhang, [Data-driven Security and Privacy (DSP) Lab, University of California, Irvine](https://faculty.sites.uci.edu/zhouli/research/) when a conditional forwarder zone uses a forwarder controlled by an attacker or uses UDP/TCP forwarder protocol that the attacker can perform MiTM. - Block Page App: Added support for automatic self signed certificate to allow showing block page for HTTPS websites. - Drop Requests App: Added option to drop malformed DNS requests. - Query Logs App: Fixed minor issue which caused the query logs request to fail when a domain with invalid character was logged in the database. - Advanced Blocking App: Fixed bug in loading regex block list which caused the app to not block the domain names as expected. - Added logging in DNS server to know why a zone transfer request was refused by the server. - Added more environment variables for use with Docker to initialize the DNS server config. Read the [environment variable documentation](https://github.com/TechnitiumSoftware/DnsServer/blob/master/DockerEnvironmentVariables.md) for complete details. - Multiple other minor bug fixes and improvements. ## Version 7.0 Release Date: 2 October 2021 - DNS Apps design updated to allow apps to act as authoritative zones, drop requests, and log queries in addition to the existing APP records in authoritative zones. - This release is a major update for DNS Apps design and thus any previously installed apps will fail to load after the update. A manual update is required to install the latest app update from the DNS App Store for these apps to work with this new release. - Advanced Blocking App: This new app allows blocking domain names based on IP address or subnet of the clients by creating groups. It also supports blocking using regex and also supports loading blocked domains from Adblock format lists. - Block Page App: This new app runs a built-in web server to allow serving a block page to clients when a domain name is blocked. - Drop Requests App: This new app allows dropping requests that match the blocked questions in the config allowing to block DNS amplification attacks that use specific domain name and query types. - NX Domain App: This new app allows blocking domain names with a NXDOMAIN response. - Query Logs (Sqlite): This new app allows logging all queries that the DNS server receives into a Sqlite database. The DNS server web panel adds an Query Logs option to allow querying the app for logged data. - Failover App: Implemented under maintenance feature to indicate if an address is taken down for maintenance. - Added Ping check option in DHCP scopes to allow detecting if an IP address is already in use before leasing it. - Added option to allow removing an allocated DHCP lease. - This release updates many API calls which may cause issues in 3rd party clients if they are not updated before deploying this new version. It is recommended to check the API documentation for changes before deploying this new release. - Multiple other minor bug fixes and improvements. ## Version 6.4.1 Release Date: 21 August 2021 - Implemented Delegation Revalidation [draft-ietf-dnsop-ns-revalidation-01](https://datatracker.ietf.org/doc/draft-ietf-dnsop-ns-revalidation/) in recursive resolver. - Fixed issues with DNS-over-TLS due to "dot" ALPN causing SSL handshake to fail when using NextDNS as forwarder. - Fixed issues in counting total unique clients in dashboard stats. The future data for total clients will be displayed correctly however the bad data since last release can be fixed by deleting '/etc/dns/config/stats/202108*.dstat' files manually. - Updated allowed list URL implementation to check for domains zone wise so that subdomain names from blocked list URLs too are allowed. - Updated DNS Failover App to v1.4 to fix implementation issues. - Multiple other minor bug fixes and improvements. ## Version 6.4 Release Date: 14 August 2021 - Added DNAME record [RFC 6672](https://datatracker.ietf.org/doc/html/rfc6672) support. - Implemented incremental zone transfer (IXFR) [RFC 1995](https://datatracker.ietf.org/doc/html/rfc1995) support. - Implemented secret key transaction authentication (TSIG) [RFC 8945](https://datatracker.ietf.org/doc/html/rfc8945) support for zone transfers. - Implemented zone transfer over TLS (XFR-over-TLS) [draft-ietf-dprive-xfr-over-tls](https://datatracker.ietf.org/doc/draft-ietf-dprive-xfr-over-tls/) support. - Added advance options in Settings to control TTL values in Cache. - Added Resync button to force resync Secondary and Stub zones. - Updated query rate limiting feature to allow limiting requests from the client's subnet. - Updated SplitHorizon App to support configuring CIDR networks. - Updated Failover App to fix multiple issues and added feature to auto generate health check URL from APP record domain name or specify the URL in the APP record data. - Fixed issues with log file rolling when using local time. - Multiple other minor bug fixes and improvements. - Updated few API calls which may cause issues in 3rd party clients if they are not updated before deploying this new version. ## Version 6.3 Release Date: 6 June 2021 - Added Failover App in DNS App Store. - Added comments option to DNS records in Zones. - Added Recursion ACL support to specify allowed and denied networks that can perform recursion. - Added Zone Options feature to allow configuring Zone Transfer and Notify settings per zone. - Added Queries Per Minute (QPM) Limit feature to limit the number of queries being made by an IP address. - Added feature to specify custom IP addresses for blocked domain names. - Added feature to temporarily/permanently disable blocking of domain names. - Added index page for DNS-over-HTTPS (DoH) web service that displays basic configuration information to user when DoH URL is visited using a web browser. - Fixed multiple issues in QNAME minimization implementation. - Fixed multiple DNS Client implementation issues. - Multiple other minor bug fixes and improvements. - Updated few API calls which may cause issues in 3rd party clients if they are not updated before deploying this new version. ## Version 6.2.3 Release Date: 2 May 2021 - Improved DNS Apps interface to show if updates are available in the installed apps list. - Updated stats module to truncate daily stats data to optimize memory usage. - Fixed issue with QNAME minimization caused due to missing check when response contained no answer and no authority. - Fixed issue in logger which would fail to start in certain conditions. - Updated DNS Apps to shuffle addresses in response to allow load balancing. ## Version 6.2.2 Release Date: 24 April 2021 - Fixed issues with recursive resolution. - Fixed issue in parsing AXFR response. - Fixed missing tags in responses to reflect correct stats on dashboard. - Fixed issue with web console redirection on saving settings when using a reverse proxy. - Multiple other minor bug fixes and improvements. ## Version 6.2.1 Release Date: 17 April 2021 - Updated DNS Cache serve stale implementation for better performance. - Implemented CNAME resolution optimization in DNS Cache and Auth Zone. - Fixed issue in DNS Cache caused due to missing check of the type of NS record's RDATA causing cache zone to return special cache RDATA record. - Fixed issue in DNS client caused when response greater than the buffer size is received. ## Version 6.2 Release Date: 11 April 2021 - Fixed critical bug in block list condition check causing server to respond with `RCODE=Refused` when only using Blocked zone. - Added option to respond with `RCODE=NxDomain` for blocked domains instead of returning `0.0.0.0` address. - Renamed `NameError` to `NxDomain` to make the terminology clear that the domain does not exists. Dashboard API returns JSON with new terminology so its advised to test your code before updating the server. ## Version 6.1 Release Date: 10 April 2021 - Added DNS App Store feature that list all available apps for quick and easy installation and update. - Added 'Overwrite' option in Add Record for zones. - Multiple ANAME record support added. - Added block list allowed URL feature to prevent domain names from getting added to the block list zone. - Fixed bug in ZoneTree. - Fixed bugs in DNS Apps. - Split Default DNS App into 5 independent apps that are now available on the DNS App Store. - Fixed issues in DNS Cache and updated code for memory optimization. - Upgraded all library projects to .NET 5. - Multiple other minor bug fixes and improvements. ## Version 6.0 Release Date: 13 March 2021 - Updated entire DNS code base to .NET 5 with new Windows installer. This upgrade will improve overall performance on Windows installations. - Added support for DNS Application (APP) propriety record with DNS Apps feature support. DNS Apps allows creating custom apps by 3rd party using .NET that run on the DNS server allowing the apps to process DNS requests and provide custom DNS response based on any bussiness logic. - A default DNS app (available to download separately) supports APP records capable of Split Horizon and Geolocation based responses using MaxMind's GeoIP2 City & Country databases. - Updated dashboard charts to save legend selection state. - Updated dashboard with Custom date selection option to display stats. - Added option to configure max stats days in settings. - Added option to enable/disable QNAME minimization. - Added delete existing files option in Restore settings. - Added support to store query stats data to allow DNS cache auto prefetch to refresh cache when DNS server restarts. - Updated TLS certificate implementation to allow using self signed certificates for web console, DoH, and DoT. - Added DHCP lease Reserve/Unreserve options to allow quickly reserving lease for clients. - Updated DHCP reserved lease option to allow overriding client's host name. - Fixed issues with DNS cache auto prefetch feature. - Fixed multiple issues in DNS cache. - Fixed multiple vulnerabilities causing DNS cache poisoning. - Multiple other minor bug fixes and improvements. ## Version 5.6 Release Date: 2 January 2021 - Updated standalone console app to work on .NET 5 and removing standalone .NET Framework app support. .NET 5 update will boost performance of the DNS server on all platforms. - Updated DNS and DHCP listener code to use async IO to improve performance. - Added HTTPS support for web service that provides the web console access. - Added support to change the web service local addresses. - Updated the server to allow changing DNS server end points, the web service end points, or enabling DoH or DoT services instantly without need to manually restart the main service. Basically, you do not need to restart the DNS server app at all for applying any kind of settings as all the changes are applied dynamically. - Added HTTP compression support in the main web service. - Added HTTP compression for downloading block lists. - Added option to clear and delete all dashboard stats and auto clean up old stats files from disk - Added option to delete all log files and auto clean up old log files from disk. - Added configurable option to disable logging, allow logging in local time, and to change log folder path. - Added option in settings to define the refresh interval for block lists with a manual option to force refresh all block lists. - Added support for exporting backup zip file containing selected items like config files, logs, stats, etc. and allow restoring the backup zip file without restarting the main service. - Fixed multiple issues in DHCP server's DNS record management. - Fixed bug in DNS server cache prefetching for stub and conditional forwarder zones causing the cached data to be overwritten by the prefetched output from recursive resolution. - Fixed html encoding issue in web app. - Added option in web app to list top 1000 clients, top domains and top blocked domains. - DNS cache serve stale feature made configurable with default serve stale TTL set to 3 days instead of 7 days. - Fixed issue in recursive resolver to avoid querying root servers when one of the parent zone's name servers exists in DNS cache. - Breaking changes in the `getDnsSettings` and `setDnsSettings` API calls will require API clients to update the code before updating the DNS server. - Multiple other minor bug fixes and improvements. ## Version 5.5 Release Date: 14 November 2020 - Added option to specify bootfile name for PXE booting. - Implemented DHCP vendor specific information option. - Implemented strict enforcing of exclusion list. - Fixed bug in DNS initial server name that was caused due to invalid characters in the computer name. - Added support for additional record processing for SRV records and fixed issues for NS and MX records processing. - Multiple other minor bug fixes and improvements. ## Version 5.4 Release Date: 18 October 2020 - Implemented QNAME randomization feature [draft-vixie-dnsext-dns0x20](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00). - Fixed bug causing infinite loop in certain conditions when using UDP as transport. - Fixed bug in DNS cache querying which caused the server to make unneeded queries when performing recursive resolution. - Added Create PTR Zone option when adding A or AAAA records. - Fixed issues with DHCP scope selection when using relay agent. - Implemented changes to allow changing DHCP scope IP allocation from dynamic to reserved and vice versa. - Updated DHCP scope to allow specifying Next Server Address for use with TFTP for booting. - Multiple other minor bug fixes and improvements. ## Version 5.3 Release Date: 26 September 2020 - Fixed issues with DHCP server that caused it to not work correctly with relay agents. - Updated DHCP server to support multiple scopes to work on a single network interface allowing it to provide different options for groups of devices. - Multiple other minor bug fixes and improvements. ## Version 5.2 Release Date: 6 September 2020 - Added feature to allow using `certbot` to renew TLS certificates automatically when using DNS-over-HTTPS and DNS-over-TLS. - Fixed issue in DHCP server that caused thread to block by implementing async methods. - Fixed bug in DNS client that caused QTYPE mismatch due to QNAME minimization. - Fixed issues in DNS-over-HTTPS client related to retries and http error handling. - Multiple other minor bug fixes and improvements. ## Version 5.1 Release Date: 29 August 2020 - Implemented async IO to allow the DNS server handle much higher concurrent loads. - Implemented independent thread pools for DNS web service and recursive resolver. - Fixed bug in block list downloader that caused 0 byte file downloads. - Fixed bug in DHCP server in creating reverse zone. - Multiple other minor bug fixes and improvements. ## Version 5.0.2 Release Date: 18 July 2020 - Fixed issue of missing port for "This Server" in DNS Client. - Added domain name that was blocked in the TXT record. - Fixed bugs in CNAME cloaking implementation. - Upgraded .NET Framework version to v4.8. - Multiple other minor bug fixes and improvements. ## Version 5.0.1 Release Date: 6 July 2020 - Fixed serialization bug for TXT records. - Fixed issue with reading DnsDatagram for DoH POST requests. - Fixed bug in json serialization of DnsDatagram for DoH json format. - Fixed bug in RTT calculation for DoH json Connection. ## Version 5.0 Release Date: 4 July 2020 - DNS Server local end points support to allow specifying alternate ports for UDP and TCP protocols. - DNS Server performance issues caused by thread contention fixed. - CNAME cloaking implemented to block domain names that resolve to CNAME which are blocked. - New Block List zone implementation that uses very less memory allowing to load block lists with millions of domain names even on a Raspberry Pi with 1GB RAM. - QNAME minimization support in recursive resolver [draft-ietf-dnsop-rfc7816bis-04](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-rfc7816bis-04). - ANAME propriety record support to allow using CNAME like feature at zone root. - Added primary zones with NOTIFY implementation [RFC 1996](https://datatracker.ietf.org/doc/html/rfc1996). - Added secondary zones with NOTIFY implementation [RFC 1996](https://datatracker.ietf.org/doc/html/rfc1996). - Added stub zones with feature to override records. - Added conditional forwarder zones with all protocols including DNS-over-HTTPS and DNS-over-TLS support. - Conditional forwarder zones with feature to override records. - Conditional forwarder zones with support for multiple forwarders with different sub domain names. - ByteTree based zone tree implementation which is a complete lock-less and thread safe tree allowing concurrent read and write operations. - Fixed bug in parsing large TXT records. - DNS Client with internal support for concurrent querying. This allows querying multiple forwarders simultaneously to return fastest response of all. - DNS Client with support to import records via zone transfer. - Multiple other bug fixes in DNS and DHCP modules. ================================================ FILE: DnsServer.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsServerApp", "DnsServerApp\DnsServerApp.csproj", "{ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsServerWindowsService", "DnsServerWindowsService\DnsServerWindowsService.csproj", "{7873B2B8-01BA-48BC-B4B0-0857FFD873C9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsServerCore", "DnsServerCore\DnsServerCore.csproj", "{4494B79B-588C-41F2-95AD-0897123AF154}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsServerSystemTrayApp", "DnsServerSystemTrayApp\DnsServerSystemTrayApp.csproj", "{2F91BD07-2CEE-47FA-8486-457B54612B4C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsServerCore.ApplicationCommon", "DnsServerCore.ApplicationCommon\DnsServerCore.ApplicationCommon.csproj", "{4ABB6715-A66F-482F-BA13-88CDFD33B2C5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apps", "Apps", "{938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeoContinentApp", "Apps\GeoContinentApp\GeoContinentApp.csproj", "{39C1822D-061A-43D0-93BE-6F900CC688B6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeoCountryApp", "Apps\GeoCountryApp\GeoCountryApp.csproj", "{338DDC94-6149-4A0E-A7A0-2630EA6BEA68}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeoDistanceApp", "Apps\GeoDistanceApp\GeoDistanceApp.csproj", "{1FE525BD-16BC-4F64-B31C-4E5AF70317A6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SplitHorizonApp", "Apps\SplitHorizonApp\SplitHorizonApp.csproj", "{CACE22C7-02A2-4579-BBC7-39F544CAD1A5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WhatIsMyDnsApp", "Apps\WhatIsMyDnsApp\WhatIsMyDnsApp.csproj", "{F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FailoverApp", "Apps\FailoverApp\FailoverApp.csproj", "{099D27AF-3AEB-495A-A5D0-46DA59CC9213}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DropRequestsApp", "Apps\DropRequestsApp\DropRequestsApp.csproj", "{738079D1-FA5A-40CD-8A27-D831919EE209}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryLogsSqliteApp", "Apps\QueryLogsSqliteApp\QueryLogsSqliteApp.csproj", "{186DEF23-863E-4954-BE16-5E5FCA75ECA2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogExporterApp", "Apps\LogExporterApp\LogExporterApp.csproj", "{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedBlockingApp", "Apps\AdvancedBlockingApp\AdvancedBlockingApp.csproj", "{A4C31093-CA65-42D4-928A-11907076C0DE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NxDomainApp", "Apps\NxDomainApp\NxDomainApp.csproj", "{BB0010FC-20E9-4397-BF9B-C9955D9AD339}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlockPageApp", "Apps\BlockPageApp\BlockPageApp.csproj", "{45C6F9AD-57D6-4D6D-9498-10B5C828E47E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WildIpApp", "Apps\WildIpApp\WildIpApp.csproj", "{8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoDataApp", "Apps\NoDataApp\NoDataApp.csproj", "{BE08D981-DDB0-4314-A571-D68EDF0F3971}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dns64App", "Apps\Dns64App\Dns64App.csproj", "{3514C4B4-78C1-46A1-82D5-4E676DD114FA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsBlockListApp", "Apps\DnsBlockListApp\DnsBlockListApp.csproj", "{9F2EC41F-6A9E-47C4-B47B-75190D5B6903}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedForwardingApp", "Apps\AdvancedForwardingApp\AdvancedForwardingApp.csproj", "{42DD2C37-4082-4E33-9AB0-04A97290D5B7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoPtrApp", "Apps\AutoPtrApp\AutoPtrApp.csproj", "{06AB9E37-5532-4CDB-8D6C-D3575594CC7E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeightedRoundRobinApp", "Apps\WeightedRoundRobinApp\WeightedRoundRobinApp.csproj", "{29688452-F88A-49F5-9C98-BE7B2812C522}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZoneAliasApp", "Apps\ZoneAliasApp\ZoneAliasApp.csproj", "{6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NxDomainOverrideApp", "Apps\NxDomainOverrideApp\NxDomainOverrideApp.csproj", "{44B057A5-3BF6-412F-8B86-D1C854CB3973}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DefaultRecordsApp", "Apps\DefaultRecordsApp\DefaultRecordsApp.csproj", "{BCA1D22A-058D-4817-8BFC-6125478C30BA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsRebindingProtectionApp", "Apps\DnsRebindingProtectionApp\DnsRebindingProtectionApp.csproj", "{159014D9-662B-429E-8006-495A9B99B902}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterAaaaApp", "Apps\FilterAaaaApp\FilterAaaaApp.csproj", "{0A9B7F39-80DA-4084-AD47-8707576927ED}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryLogsSqlServerApp", "Apps\QueryLogsSqlServerApp\QueryLogsSqlServerApp.csproj", "{6F655C97-FD43-4FE1-B15A-6C783D2D91C9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryLogsMySqlApp", "Apps\QueryLogsMySqlApp\QueryLogsMySqlApp.csproj", "{699E2A1D-D917-4825-939E-65CDB2B16A96}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MispConnectorApp", "Apps\MispConnectorApp\MispConnectorApp.csproj", "{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DnsServerCore.HttpApi", "DnsServerCore.HttpApi\DnsServerCore.HttpApi.csproj", "{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Debug|Any CPU.Build.0 = Debug|Any CPU {ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Release|Any CPU.ActiveCfg = Release|Any CPU {ADE80805-9FA7-4F66-8A18-57B98F8C0B0F}.Release|Any CPU.Build.0 = Release|Any CPU {7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {7873B2B8-01BA-48BC-B4B0-0857FFD873C9}.Release|Any CPU.Build.0 = Release|Any CPU {4494B79B-588C-41F2-95AD-0897123AF154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4494B79B-588C-41F2-95AD-0897123AF154}.Debug|Any CPU.Build.0 = Debug|Any CPU {4494B79B-588C-41F2-95AD-0897123AF154}.Release|Any CPU.ActiveCfg = Release|Any CPU {4494B79B-588C-41F2-95AD-0897123AF154}.Release|Any CPU.Build.0 = Release|Any CPU {2F91BD07-2CEE-47FA-8486-457B54612B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F91BD07-2CEE-47FA-8486-457B54612B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F91BD07-2CEE-47FA-8486-457B54612B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F91BD07-2CEE-47FA-8486-457B54612B4C}.Release|Any CPU.Build.0 = Release|Any CPU {4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {4ABB6715-A66F-482F-BA13-88CDFD33B2C5}.Release|Any CPU.Build.0 = Release|Any CPU {39C1822D-061A-43D0-93BE-6F900CC688B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {39C1822D-061A-43D0-93BE-6F900CC688B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {39C1822D-061A-43D0-93BE-6F900CC688B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {39C1822D-061A-43D0-93BE-6F900CC688B6}.Release|Any CPU.Build.0 = Release|Any CPU {338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Debug|Any CPU.Build.0 = Debug|Any CPU {338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Release|Any CPU.ActiveCfg = Release|Any CPU {338DDC94-6149-4A0E-A7A0-2630EA6BEA68}.Release|Any CPU.Build.0 = Release|Any CPU {1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FE525BD-16BC-4F64-B31C-4E5AF70317A6}.Release|Any CPU.Build.0 = Release|Any CPU {CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {CACE22C7-02A2-4579-BBC7-39F544CAD1A5}.Release|Any CPU.Build.0 = Release|Any CPU {F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0}.Release|Any CPU.Build.0 = Release|Any CPU {099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Debug|Any CPU.Build.0 = Debug|Any CPU {099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Release|Any CPU.ActiveCfg = Release|Any CPU {099D27AF-3AEB-495A-A5D0-46DA59CC9213}.Release|Any CPU.Build.0 = Release|Any CPU {738079D1-FA5A-40CD-8A27-D831919EE209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {738079D1-FA5A-40CD-8A27-D831919EE209}.Debug|Any CPU.Build.0 = Debug|Any CPU {738079D1-FA5A-40CD-8A27-D831919EE209}.Release|Any CPU.ActiveCfg = Release|Any CPU {738079D1-FA5A-40CD-8A27-D831919EE209}.Release|Any CPU.Build.0 = Release|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.Build.0 = Release|Any CPU {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.Build.0 = Release|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.Build.0 = Release|Any CPU {BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB0010FC-20E9-4397-BF9B-C9955D9AD339}.Release|Any CPU.Build.0 = Release|Any CPU {45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Debug|Any CPU.Build.0 = Debug|Any CPU {45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Release|Any CPU.ActiveCfg = Release|Any CPU {45C6F9AD-57D6-4D6D-9498-10B5C828E47E}.Release|Any CPU.Build.0 = Release|Any CPU {8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B6BEB00-0AC2-4680-A848-31AD8A0FCD82}.Release|Any CPU.Build.0 = Release|Any CPU {BE08D981-DDB0-4314-A571-D68EDF0F3971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BE08D981-DDB0-4314-A571-D68EDF0F3971}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE08D981-DDB0-4314-A571-D68EDF0F3971}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE08D981-DDB0-4314-A571-D68EDF0F3971}.Release|Any CPU.Build.0 = Release|Any CPU {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {3514C4B4-78C1-46A1-82D5-4E676DD114FA}.Release|Any CPU.Build.0 = Release|Any CPU {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F2EC41F-6A9E-47C4-B47B-75190D5B6903}.Release|Any CPU.Build.0 = Release|Any CPU {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {42DD2C37-4082-4E33-9AB0-04A97290D5B7}.Release|Any CPU.Build.0 = Release|Any CPU {06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU {06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU {06AB9E37-5532-4CDB-8D6C-D3575594CC7E}.Release|Any CPU.Build.0 = Release|Any CPU {29688452-F88A-49F5-9C98-BE7B2812C522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29688452-F88A-49F5-9C98-BE7B2812C522}.Debug|Any CPU.Build.0 = Debug|Any CPU {29688452-F88A-49F5-9C98-BE7B2812C522}.Release|Any CPU.ActiveCfg = Release|Any CPU {29688452-F88A-49F5-9C98-BE7B2812C522}.Release|Any CPU.Build.0 = Release|Any CPU {6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Debug|Any CPU.Build.0 = Debug|Any CPU {6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Release|Any CPU.ActiveCfg = Release|Any CPU {6FF8C5F7-C98E-41C1-8FCD-25AEA0057673}.Release|Any CPU.Build.0 = Release|Any CPU {44B057A5-3BF6-412F-8B86-D1C854CB3973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44B057A5-3BF6-412F-8B86-D1C854CB3973}.Debug|Any CPU.Build.0 = Debug|Any CPU {44B057A5-3BF6-412F-8B86-D1C854CB3973}.Release|Any CPU.ActiveCfg = Release|Any CPU {44B057A5-3BF6-412F-8B86-D1C854CB3973}.Release|Any CPU.Build.0 = Release|Any CPU {BCA1D22A-058D-4817-8BFC-6125478C30BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BCA1D22A-058D-4817-8BFC-6125478C30BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCA1D22A-058D-4817-8BFC-6125478C30BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCA1D22A-058D-4817-8BFC-6125478C30BA}.Release|Any CPU.Build.0 = Release|Any CPU {159014D9-662B-429E-8006-495A9B99B902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {159014D9-662B-429E-8006-495A9B99B902}.Debug|Any CPU.Build.0 = Debug|Any CPU {159014D9-662B-429E-8006-495A9B99B902}.Release|Any CPU.ActiveCfg = Release|Any CPU {159014D9-662B-429E-8006-495A9B99B902}.Release|Any CPU.Build.0 = Release|Any CPU {0A9B7F39-80DA-4084-AD47-8707576927ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A9B7F39-80DA-4084-AD47-8707576927ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A9B7F39-80DA-4084-AD47-8707576927ED}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A9B7F39-80DA-4084-AD47-8707576927ED}.Release|Any CPU.Build.0 = Release|Any CPU {6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F655C97-FD43-4FE1-B15A-6C783D2D91C9}.Release|Any CPU.Build.0 = Release|Any CPU {699E2A1D-D917-4825-939E-65CDB2B16A96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {699E2A1D-D917-4825-939E-65CDB2B16A96}.Debug|Any CPU.Build.0 = Debug|Any CPU {699E2A1D-D917-4825-939E-65CDB2B16A96}.Release|Any CPU.ActiveCfg = Release|Any CPU {699E2A1D-D917-4825-939E-65CDB2B16A96}.Release|Any CPU.Build.0 = Release|Any CPU {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}.Release|Any CPU.Build.0 = Release|Any CPU {1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {39C1822D-061A-43D0-93BE-6F900CC688B6} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {338DDC94-6149-4A0E-A7A0-2630EA6BEA68} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {1FE525BD-16BC-4F64-B31C-4E5AF70317A6} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {CACE22C7-02A2-4579-BBC7-39F544CAD1A5} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {F5FD0B99-08AC-47FC-8C52-7D7CA1A7D9A0} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {099D27AF-3AEB-495A-A5D0-46DA59CC9213} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {738079D1-FA5A-40CD-8A27-D831919EE209} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {186DEF23-863E-4954-BE16-5E5FCA75ECA2} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {A4C31093-CA65-42D4-928A-11907076C0DE} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {BB0010FC-20E9-4397-BF9B-C9955D9AD339} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {45C6F9AD-57D6-4D6D-9498-10B5C828E47E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {8B6BEB00-0AC2-4680-A848-31AD8A0FCD82} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {BE08D981-DDB0-4314-A571-D68EDF0F3971} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {3514C4B4-78C1-46A1-82D5-4E676DD114FA} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {9F2EC41F-6A9E-47C4-B47B-75190D5B6903} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {42DD2C37-4082-4E33-9AB0-04A97290D5B7} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {06AB9E37-5532-4CDB-8D6C-D3575594CC7E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {29688452-F88A-49F5-9C98-BE7B2812C522} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {6FF8C5F7-C98E-41C1-8FCD-25AEA0057673} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {44B057A5-3BF6-412F-8B86-D1C854CB3973} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {BCA1D22A-058D-4817-8BFC-6125478C30BA} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {159014D9-662B-429E-8006-495A9B99B902} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {0A9B7F39-80DA-4084-AD47-8707576927ED} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {6F655C97-FD43-4FE1-B15A-6C783D2D91C9} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {699E2A1D-D917-4825-939E-65CDB2B16A96} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {83C8180A-0F86-F9A0-8F41-6FD61FAC41CB} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6747BB6D-2826-4356-A213-805FBCCF9201} EndGlobalSection EndGlobal ================================================ FILE: DnsServerApp/DnsServerApp.csproj ================================================  false true Exe net9.0 enable logo2.ico 14.3 false Technitium Technitium DNS Server Shreyas Zare DnsServerApp DnsServerApp DnsServerApp.Program https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: DnsServerApp/Program.cs ================================================ /* 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; using System; using System.Threading; using System.Threading.Tasks; using System.Runtime.InteropServices; namespace DnsServerApp { class Program { static async Task Main(string[] args) { bool throwIfBindFails = false; string? configFolder = null; foreach (string arg in args) { switch (arg) { case "--icu-test": _ = System.Globalization.CultureInfo.CurrentCulture; return; case "--stop-if-bind-fails": throwIfBindFails = true; break; default: configFolder = arg; break; } } ManualResetEvent waitHandle = new ManualResetEvent(false); ManualResetEvent exitHandle = new ManualResetEvent(false); DnsWebService? service = null; PosixSignalRegistration? psr = null; try { Uri updateCheckUri; switch (Environment.OSVersion.Platform) { case PlatformID.Win32NT: updateCheckUri = new Uri("https://go.technitium.com/?id=41"); break; default: updateCheckUri = new Uri("https://go.technitium.com/?id=42"); break; } service = new DnsWebService(configFolder, updateCheckUri); await service.StartAsync(throwIfBindFails); Console.CancelKeyPress += delegate (object? sender, ConsoleCancelEventArgs e) { e.Cancel = true; waitHandle.Set(); }; AppDomain.CurrentDomain.ProcessExit += delegate (object? sender, EventArgs e) { waitHandle.Set(); exitHandle.WaitOne(); }; if (Environment.OSVersion.Platform == PlatformID.Unix) { psr = PosixSignalRegistration.Create(PosixSignal.SIGTERM, delegate (PosixSignalContext context) { waitHandle.Set(); exitHandle.WaitOne(); }); } Console.WriteLine("Technitium DNS Server was started successfully.\r\nUsing config folder: " + service.ConfigFolder + "\r\n\r\nNote: Open http://" + Environment.MachineName.ToLowerInvariant() + ":" + service.WebServiceHttpPort + "/ in web browser to access web console.\r\n\r\nPress [CTRL + C] to stop..."); waitHandle.WaitOne(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { Console.WriteLine("\r\nTechnitium DNS Server is stopping..."); service?.Dispose(); psr?.Dispose(); Console.WriteLine("Technitium DNS Server was stopped successfully."); exitHandle.Set(); } } } } ================================================ FILE: DnsServerApp/Properties/PublishProfiles/FolderProfile.pubxml ================================================  Release Any CPU bin\Release\publish\ FileSystem <_TargetId>Folder net9.0 false ================================================ FILE: DnsServerApp/install.sh ================================================ #!/bin/sh dotnetDir="/opt/dotnet" dotnetVersion="9.0" dotnetRuntime="Microsoft.AspNetCore.App 9.0." dotnetUrl="https://dot.net/v1/dotnet-install.sh" if [ -d "/etc/dns/config" ] then dnsDir="/etc/dns" else dnsDir="/opt/technitium/dns" fi dnsConfig="/etc/dns" dnsTar="$dnsDir/DnsServerPortable.tar.gz" dnsUrl="https://download.technitium.com/dns/DnsServerPortable.tar.gz" installLog="$dnsDir/install.log" echo "" echo "===============================" echo "Technitium DNS Server Installer" echo "===============================" echo "" mkdir -p $dnsDir echo "" > $installLog if dotnet --list-runtimes 2> /dev/null | grep -q "$dotnetRuntime"; then dotnetFound="yes" else dotnetFound="no" fi if [ ! -d $dotnetDir ] && [ "$dotnetFound" = "yes" ] then echo "ASP.NET Core Runtime is already installed." else if [ -d $dotnetDir ] && [ "$dotnetFound" = "yes" ] then dotnetUpdate="yes" echo "Updating ASP.NET Core Runtime..." else dotnetUpdate="no" echo "Installing ASP.NET Core Runtime..." fi curl -sSL $dotnetUrl | bash /dev/stdin -c $dotnetVersion --runtime aspnetcore --no-path --install-dir $dotnetDir --verbose >> $installLog 2>&1 if [ ! -f "/usr/bin/dotnet" ] then ln -s $dotnetDir/dotnet /usr/bin >> $installLog 2>&1 fi if dotnet --list-runtimes 2> /dev/null | grep -q "$dotnetRuntime"; then if [ "$dotnetUpdate" = "yes" ] then echo "ASP.NET Core Runtime was updated successfully!" else echo "ASP.NET Core Runtime was installed successfully!" fi else echo "Failed to install ASP.NET Core Runtime. Please check '$installLog' for details." exit 1 fi fi echo "" echo "Downloading Technitium DNS Server..." if ! curl -o $dnsTar --fail $dnsUrl >> $installLog 2>&1 then echo "Failed to download Technitium DNS Server from: $dnsUrl" echo "Please check '$installLog' for details." exit 1 fi if [ -d $dnsConfig ] then echo "Updating Technitium DNS Server..." else echo "Installing Technitium DNS Server..." fi tar -zxf $dnsTar -C $dnsDir >> $installLog 2>&1 echo "" if dotnet $dnsDir/DnsServerApp.dll --icu-test >> $installLog 2>&1 then echo "ICU package is already installed." else echo "Checking for required ICU package..." if command -v apt-get >/dev/null 2>&1; then # Debian/Ubuntu based if ! dpkg -l | grep -q "libicu"; then echo "Installing required ICU package..." apt-get update >> $installLog 2>&1 # Try to install the most common package name if apt-cache show libicu74 >/dev/null 2>&1; then echo "Installing libicu74 package..." apt-get install -y libicu74 >> $installLog 2>&1 elif apt-cache show libicu72 >/dev/null 2>&1; then echo "Installing libicu72 package..." apt-get install -y libicu72 >> $installLog 2>&1 elif apt-cache show libicu70 >/dev/null 2>&1; then echo "Installing libicu70 package..." apt-get install -y libicu70 >> $installLog 2>&1 else # Fallback to a generic approach echo "No specific libicu package was found, trying generic installation..." apt-get install -y libicu* >> $installLog 2>&1 fi fi elif command -v dnf >/dev/null 2>&1; then # Fedora/RHEL based if ! rpm -qa | grep -q "libicu"; then echo "Installing required ICU package..." dnf install -y libicu >> $installLog 2>&1 fi elif command -v yum >/dev/null 2>&1; then # Older RHEL/CentOS systems if ! rpm -qa | grep -q "libicu"; then echo "Installing required ICU package..." yum install -y libicu >> $installLog 2>&1 fi elif command -v zypper >/dev/null 2>&1; then # openSUSE based if ! rpm -qa | grep -q "libicu"; then echo "Installing required ICU package..." zypper install -y libicu >> $installLog 2>&1 fi elif command -v pacman >/dev/null 2>&1; then # Arch based if ! pacman -Q | grep -q "icu"; then echo "Installing required ICU package..." pacman -S --noconfirm icu >> $installLog 2>&1 fi elif command -v apk >/dev/null 2>&1; then # Alpine Linux if ! apk list --installed | grep -q "icu"; then echo "Installing required ICU package..." apk add --no-cache icu >> $installLog 2>&1 fi else echo "Failed to install Technitium DNS Server: could not determine package manager to install ICU package. Please install ICU package manually and try again." echo "Please read the 'Missing ICU Package' section in this blog post to understand how to manually install the ICU package for your distro: https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html" exit 1 fi #test again to confirm if dotnet $dnsDir/DnsServerApp.dll --icu-test >> $installLog 2>&1 then echo "ICU package was installed successfully!" else echo "Failed to install Technitium DNS Server: failed to install ICU package. Please install ICU package manually and try again." echo "Please read the 'Missing ICU Package' section in this blog post to understand how to manually install the ICU package for your distro: https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html" exit 1 fi fi echo "" if ! [ "$(ps --no-headers -o comm 1 | tr -d '\n')" = "systemd" ] then echo "Failed to install Technitium DNS Server: systemd was not detected." echo "Please read the 'Installing DNS Server Manually' section in this blog post to understand how to manually install the DNS server on your distro: https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html" exit 1 fi if [ -f "/etc/systemd/system/dns.service" ] then echo "Restarting systemd service..." systemctl restart dns.service >> $installLog 2>&1 else echo "Configuring systemd service..." cp $dnsDir/systemd.service /etc/systemd/system/dns.service systemctl enable dns.service >> $installLog 2>&1 systemctl stop systemd-resolved >> $installLog 2>&1 systemctl disable systemd-resolved >> $installLog 2>&1 systemctl start dns.service >> $installLog 2>&1 rm /etc/resolv.conf >> $installLog 2>&1 echo -e "# Generated by Technitium DNS Server Installer\n\nnameserver 127.0.0.1" > /etc/resolv.conf 2>> $installLog if [ -f "/etc/NetworkManager/NetworkManager.conf" ] then echo -e "[main]\ndns=default" >> /etc/NetworkManager/NetworkManager.conf 2>> $installLog fi fi echo "" echo "Technitium DNS Server was installed successfully!" echo "Open http://$(cat /proc/sys/kernel/hostname):5380/ to access the web console." echo "" echo "Donate! Make a contribution by becoming a Patron: https://www.patreon.com/technitium" echo "" ================================================ FILE: DnsServerApp/start.bat ================================================ dotnet DnsServerApp.dll ================================================ FILE: DnsServerApp/start.sh ================================================ #!/bin/sh dotnet DnsServerApp.dll ================================================ FILE: DnsServerApp/systemd.service ================================================ [Unit] Description=Technitium DNS Server [Service] WorkingDirectory=/opt/technitium/dns ExecStart=/usr/bin/dotnet /opt/technitium/dns/DnsServerApp.dll /etc/dns Restart=always # Restart service after 10 seconds if the dotnet service crashes: RestartSec=10 SyslogIdentifier=dns-server [Install] WantedBy=multi-user.target ================================================ FILE: DnsServerApp/uninstall.sh ================================================ #!/bin/sh dotnetDir="/opt/dotnet" if [ -d "/etc/dns/config" ] then dnsDir="/etc/dns" else dnsDir="/opt/technitium/dns" fi echo "" echo "=================================" echo "Technitium DNS Server Uninstaller" echo "=================================" echo "" echo "Uninstalling Technitium DNS Server..." if [ -d $dnsDir ] then if [ "$(ps --no-headers -o comm 1 | tr -d '\n')" = "systemd" ] then sudo systemctl disable dns.service >/dev/null 2>&1 sudo systemctl stop dns.service >/dev/null 2>&1 rm /etc/systemd/system/dns.service >/dev/null 2>&1 rm /etc/resolv.conf >/dev/null 2>&1 echo "nameserver 8.8.8.8" >> /etc/resolv.conf echo "nameserver 1.1.1.1" >> /etc/resolv.conf fi rm -rf $dnsDir >/dev/null 2>&1 if [ -d $dotnetDir ] then echo "Uninstalling .NET Runtime..." rm /usr/bin/dotnet >/dev/null 2>&1 rm -rf $dotnetDir >/dev/null 2>&1 fi fi echo "" echo "Thank you for using Technitium DNS Server!" ================================================ FILE: DnsServerCore/Auth/AuthManager.cs ================================================ /* 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 System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Security.OTP; namespace DnsServerCore.Auth { sealed class AuthManager : IDisposable { #region variables ConcurrentDictionary _groups = new ConcurrentDictionary(1, 4); ConcurrentDictionary _users = new ConcurrentDictionary(1, 4); ConcurrentDictionary _permissions = new ConcurrentDictionary(1, 11); ConcurrentDictionary _sessions = new ConcurrentDictionary(1, 10); readonly ConcurrentDictionary _failedLoginAttemptNetworks = new ConcurrentDictionary(1, 10); const int MAX_LOGIN_ATTEMPTS = 5; readonly ConcurrentDictionary _blockedNetworks = new ConcurrentDictionary(1, 10); const int BLOCK_NETWORK_INTERVAL = 5 * 60 * 1000; readonly string _configFolder; readonly LogManager _log; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor public AuthManager(string configFolder, LogManager log) { _configFolder = configFolder; _log = log; _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); } } } }); LoadConfigFile(); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; lock (_saveLock) { _saveTimer?.Dispose(); //always save config here to write user login timestamps details try { SaveConfigFileInternal(); } catch (Exception ex) { _log.Write(ex); } finally { _pendingSave = false; } } _disposed = true; } #endregion #region config private void LoadConfigFile() { string configFile = Path.Combine(_configFolder, "auth.config"); try { bool passwordResetOption = false; if (!File.Exists(configFile)) { string passwordResetConfigFile = Path.Combine(_configFolder, "resetadmin.config"); if (File.Exists(passwordResetConfigFile)) { passwordResetOption = true; configFile = passwordResetConfigFile; } } using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS, false); } _log.Write("DNS Server auth config file was loaded: " + configFile); if (passwordResetOption) { User adminUser = GetUser("admin"); if (adminUser is null) { adminUser = CreateUser("Administrator", "admin", "admin"); } else { adminUser.ChangePassword("admin"); adminUser.Disabled = false; if (adminUser.TOTPEnabled) adminUser.DisableTOTP(); } adminUser.AddToGroup(GetGroup(Group.ADMINISTRATORS)); _log.Write("DNS Server has reset the password for user: admin"); SaveConfigFileInternal(); try { File.Delete(configFile); } catch { } } } catch (FileNotFoundException) { CreateDefaultConfig(); SaveConfigFileInternal(); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading auth config file: " + configFile + "\r\n" + ex.ToString()); _log.Write("Note: You may try deleting the auth config file to fix this issue. However, you will lose auth settings but, rest of the DNS settings and zone data wont be affected."); throw; } } public void LoadOldConfig(string password, bool isPasswordHash) { User user = GetUser("admin"); if (user is null) user = CreateUser("Administrator", "admin", "admin"); user.AddToGroup(GetGroup(Group.ADMINISTRATORS)); if (isPasswordHash) user.LoadOldSchemeCredentials(password); else user.ChangePassword(password); lock (_saveLock) { SaveConfigFileInternal(); } } public void LoadConfig(Stream s, bool isConfigTransfer, UserSession implantSession = null) { lock (_saveLock) { ReadConfigFrom(s, isConfigTransfer); if (!isConfigTransfer) { if (implantSession is not null) { //implant current user and session into config while restoring backup config using (MemoryStream mS = new MemoryStream()) { //implant current user implantSession.User.WriteTo(new BinaryWriter(mS)); mS.Position = 0; User newUser = new User(new BinaryReader(mS), _groups); newUser.AddToGroup(GetGroup(Group.ADMINISTRATORS)); _users[newUser.Username] = newUser; //implant current session mS.SetLength(0); implantSession.WriteTo(new BinaryWriter(mS)); mS.Position = 0; UserSession newSession = new UserSession(new BinaryReader(mS), _users); _sessions[newSession.Token] = newSession; } } } //save config file SaveConfigFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void SaveConfigFileInternal() { string configFile = Path.Combine(_configFolder, "auth.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("DNS Server auth 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 ReadConfigFrom(Stream s, bool isConfigTransfer) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "AS") //format throw new InvalidDataException("DNS Server auth config file format is invalid."); ConcurrentDictionary groups = new ConcurrentDictionary(1, 4); ConcurrentDictionary users = new ConcurrentDictionary(1, 4); ConcurrentDictionary permissions = new ConcurrentDictionary(1, 11); ConcurrentDictionary sessions = new ConcurrentDictionary(1, 10); int version = bR.ReadByte(); switch (version) { case 1: { int count = bR.ReadByte(); for (int i = 0; i < count; i++) { Group group = new Group(bR); groups.TryAdd(group.Name.ToLowerInvariant(), group); } } { int count = bR.ReadByte(); for (int i = 0; i < count; i++) { User user = new User(bR, groups); users.TryAdd(user.Username, user); } } { int count = bR.ReadInt32(); for (int i = 0; i < count; i++) { Permission permission = new Permission(bR, users, groups); permissions.TryAdd(permission.Section, permission); } } { int count = bR.ReadInt32(); for (int i = 0; i < count; i++) { UserSession session = new UserSession(bR, users); if (!session.HasExpired()) sessions.TryAdd(session.Token, session); } } break; default: throw new InvalidDataException("DNS Server auth config version not supported."); } _groups = groups; _users = users; if (isConfigTransfer) { //sync only required permissions from newly loaded config foreach (KeyValuePair permission in permissions) { switch (permission.Key) { case PermissionSection.Zones: //sync user and group permissions as-is for zones section Permission zonesPermission = _permissions[PermissionSection.Zones]; zonesPermission.SyncPermissions(permission.Value.UserPermissions); zonesPermission.SyncPermissions(permission.Value.GroupPermissions); break; default: _permissions[permission.Key] = permission.Value; break; } } //update all user objects in existing sessions to reflect the newly loaded config foreach (KeyValuePair session in _sessions) session.Value.UpdateUserObject(_users); //sync only API sessions from newly loaded config foreach (KeyValuePair existingSession in _sessions) { if (existingSession.Value.Type == UserSessionType.ApiToken) { if (!sessions.ContainsKey(existingSession.Key)) _sessions.TryRemove(existingSession); } } foreach (KeyValuePair session in sessions) { if (session.Value.Type == UserSessionType.ApiToken) _sessions[session.Key] = session.Value; } } else { _permissions = permissions; _sessions = sessions; } } private void WriteConfigTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("AS")); //format bW.Write((byte)1); //version bW.Write(Convert.ToByte(_groups.Count)); foreach (KeyValuePair group in _groups) group.Value.WriteTo(bW); bW.Write(Convert.ToByte(_users.Count)); foreach (KeyValuePair user in _users) user.Value.WriteTo(bW); bW.Write(_permissions.Count); foreach (KeyValuePair permission in _permissions) permission.Value.WriteTo(bW); List activeSessions = new List(_sessions.Count); foreach (KeyValuePair session in _sessions) { if (session.Value.HasExpired()) _sessions.TryRemove(session.Key, out _); else activeSessions.Add(session.Value); } bW.Write(activeSessions.Count); foreach (UserSession session in activeSessions) session.WriteTo(bW); } #endregion #region private private void CreateDefaultConfig() { Group adminGroup = CreateGroup(Group.ADMINISTRATORS, "Super administrators"); Group dnsAdminGroup = CreateGroup(Group.DNS_ADMINISTRATORS, "DNS service administrators"); Group dhcpAdminGroup = CreateGroup(Group.DHCP_ADMINISTRATORS, "DHCP service administrators"); Group everyoneGroup = CreateGroup(Group.EVERYONE, "All users"); SetPermission(PermissionSection.Dashboard, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Zones, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Cache, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Allowed, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Blocked, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Apps, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.DnsClient, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Settings, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.DhcpServer, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Administration, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Logs, adminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Zones, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Cache, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Allowed, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Blocked, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Apps, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.DnsClient, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Settings, dnsAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.DhcpServer, dhcpAdminGroup, PermissionFlag.ViewModifyDelete); SetPermission(PermissionSection.Dashboard, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.Zones, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.Cache, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.Allowed, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.Blocked, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.Apps, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.DnsClient, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.DhcpServer, everyoneGroup, PermissionFlag.View); SetPermission(PermissionSection.Logs, everyoneGroup, PermissionFlag.View); string adminPassword = Environment.GetEnvironmentVariable("DNS_SERVER_ADMIN_PASSWORD"); string adminPasswordFile = Environment.GetEnvironmentVariable("DNS_SERVER_ADMIN_PASSWORD_FILE"); User adminUser; if (!string.IsNullOrEmpty(adminPassword)) { adminUser = CreateUser("Administrator", "admin", adminPassword); } else if (!string.IsNullOrEmpty(adminPasswordFile)) { try { using (StreamReader sR = new StreamReader(adminPasswordFile, true)) { string password = sR.ReadLine(); adminUser = CreateUser("Administrator", "admin", password); } } catch (Exception ex) { _log.Write(ex); adminUser = CreateUser("Administrator", "admin", "admin"); } } else { adminUser = CreateUser("Administrator", "admin", "admin"); } adminUser.AddToGroup(adminGroup); } private async Task AuthenticateUserAsync(string username, string password, string totp, IPAddress remoteAddress) { IPAddress network = GetClientNetwork(remoteAddress); if (IsNetworkBlocked(network)) throw new DnsWebServiceException("Max limit of " + MAX_LOGIN_ATTEMPTS + " attempts exceeded. Access blocked for " + (BLOCK_NETWORK_INTERVAL / 1000) + " seconds."); User user = GetUser(username); if ((user is null) || !user.PasswordHash.Equals(user.GetPasswordHashFor(password), StringComparison.Ordinal)) { if (password != "admin") { MarkFailedLoginAttempt(network); if (HasLoginAttemptExceedLimit(network, MAX_LOGIN_ATTEMPTS)) BlockNetwork(network, BLOCK_NETWORK_INTERVAL); } await Task.Delay(1000); throw new DnsWebServiceException("Invalid username or password for user: " + username); } if (user.TOTPEnabled) { if (string.IsNullOrEmpty(totp)) throw new TwoFactorAuthRequiredWebServiceException("A time-based one-time password (TOTP) is required for user: " + username); Authenticator authenticator = new Authenticator(user.TOTPKeyUri); if (!authenticator.IsTOTPValid(totp)) { MarkFailedLoginAttempt(network); if (HasLoginAttemptExceedLimit(network, MAX_LOGIN_ATTEMPTS)) BlockNetwork(network, BLOCK_NETWORK_INTERVAL); await Task.Delay(1000); throw new DnsWebServiceException("Invalid time-based one-time password (TOTP) was attempted for user: " + username); } } ResetFailedLoginAttempts(network); if (user.Disabled) throw new DnsWebServiceException("User account is disabled. Please contact your administrator."); return user; } private static IPAddress GetClientNetwork(IPAddress address) { switch (address.AddressFamily) { case AddressFamily.InterNetwork: return address.GetNetworkAddress(32); case AddressFamily.InterNetworkV6: return address.GetNetworkAddress(64); default: throw new InvalidOperationException(); } } private void MarkFailedLoginAttempt(IPAddress network) { _failedLoginAttemptNetworks.AddOrUpdate(network, 1, delegate (IPAddress key, int attempts) { return attempts + 1; }); } private bool HasLoginAttemptExceedLimit(IPAddress network, int limit) { if (!_failedLoginAttemptNetworks.TryGetValue(network, out int attempts)) return false; return attempts >= limit; } private void ResetFailedLoginAttempts(IPAddress network) { _failedLoginAttemptNetworks.TryRemove(network, out _); } private void BlockNetwork(IPAddress network, int interval) { _blockedNetworks.TryAdd(network, DateTime.UtcNow.AddMilliseconds(interval)); } private bool IsNetworkBlocked(IPAddress network) { if (!_blockedNetworks.TryGetValue(network, out DateTime expiry)) return false; if (expiry > DateTime.UtcNow) { return true; } else { UnblockNetwork(network); ResetFailedLoginAttempts(network); return false; } } private void UnblockNetwork(IPAddress network) { _blockedNetworks.TryRemove(network, out _); } #endregion #region public public User GetUser(string username) { if (_users.TryGetValue(username.ToLowerInvariant(), out User user)) return user; return null; } public User CreateUser(string displayName, string username, string password, int iterations = User.DEFAULT_ITERATIONS) { if (_users.Count >= byte.MaxValue) throw new DnsWebServiceException("Cannot create more than 255 users."); username = username.ToLowerInvariant(); User user = new User(displayName, username, password, iterations); if (_users.TryAdd(username, user)) { user.AddToGroup(GetGroup(Group.EVERYONE)); return user; } throw new DnsWebServiceException("User already exists: " + username); } public void ChangeUsername(User user, string newUsername) { if (user.Username.Equals(newUsername, StringComparison.OrdinalIgnoreCase)) return; string oldUsername = user.Username; user.Username = newUsername; if (!_users.TryAdd(user.Username, user)) { user.Username = oldUsername; //revert throw new DnsWebServiceException("User already exists: " + newUsername); } _users.TryRemove(oldUsername, out _); } public async Task ChangePasswordAsync(string username, string password, string totp, IPAddress remoteAddress, string newPassword, int iterations) { User user = await AuthenticateUserAsync(username, password, totp, remoteAddress); user.ChangePassword(newPassword, iterations); return user; } public bool DeleteUser(string username) { if (_users.TryRemove(username.ToLowerInvariant(), out User deletedUser)) { //delete all sessions foreach (UserSession session in GetSessions(deletedUser)) DeleteSession(session.Token); //delete all permissions foreach (KeyValuePair permission in _permissions) { permission.Value.RemovePermission(deletedUser); permission.Value.RemoveAllSubItemPermissions(deletedUser); } return true; } return false; } public Group GetGroup(string name) { if (_groups.TryGetValue(name.ToLowerInvariant(), out Group group)) return group; return null; } public List GetGroupMembers(Group group) { List members = new List(); foreach (KeyValuePair user in _users) { if (user.Value.IsMemberOfGroup(group)) members.Add(user.Value); } return members; } public void SyncGroupMembers(Group group, IReadOnlyDictionary users) { //remove foreach (KeyValuePair user in _users) { if (!users.ContainsKey(user.Key)) user.Value.RemoveFromGroup(group); } //set foreach (KeyValuePair user in users) user.Value.AddToGroup(group); } public Group CreateGroup(string name, string description) { if (_groups.Count >= byte.MaxValue) throw new DnsWebServiceException("Cannot create more than 255 groups."); Group group = new Group(name, description); if (_groups.TryAdd(name.ToLowerInvariant(), group)) return group; throw new DnsWebServiceException("Group already exists: " + name); } public void RenameGroup(Group group, string newGroupName) { if (group.Name.Equals(newGroupName, StringComparison.OrdinalIgnoreCase)) { group.Name = newGroupName; return; } string oldGroupName = group.Name; group.Name = newGroupName; if (!_groups.TryAdd(group.Name.ToLowerInvariant(), group)) { group.Name = oldGroupName; //revert throw new DnsWebServiceException("Group already exists: " + newGroupName); } _groups.TryRemove(oldGroupName.ToLowerInvariant(), out _); //update users foreach (KeyValuePair user in _users) user.Value.RenameGroup(oldGroupName); } public bool DeleteGroup(string name) { name = name.ToLowerInvariant(); switch (name) { case "everyone": case "administrators": case "dns administrators": case "dhcp administrators": throw new InvalidOperationException("Access was denied."); default: if (_groups.TryRemove(name, out Group deletedGroup)) { //remove all users from deleted group foreach (KeyValuePair user in _users) user.Value.RemoveFromGroup(deletedGroup); //delete all permissions foreach (KeyValuePair permission in _permissions) { permission.Value.RemovePermission(deletedGroup); permission.Value.RemoveAllSubItemPermissions(deletedGroup); } return true; } return false; } } public UserSession GetSession(string token) { if (_sessions.TryGetValue(token, out UserSession session)) return session; return null; } public List GetSessions(User user) { List userSessions = new List(); foreach (KeyValuePair session in _sessions) { if (session.Value.User.Equals(user) && !session.Value.HasExpired()) userSessions.Add(session.Value); } return userSessions; } public async Task CreateSessionAsync(UserSessionType type, string tokenName, string username, string password, string totp, IPAddress remoteAddress, string userAgent) { User user = await AuthenticateUserAsync(username, password, totp, remoteAddress); UserSession session = new UserSession(type, tokenName, user, remoteAddress, userAgent); if (!_sessions.TryAdd(session.Token, session)) throw new DnsWebServiceException("Error while creating session. Please try again."); user.LoggedInFrom(remoteAddress); return session; } public UserSession CreateApiToken(string tokenName, string username, IPAddress remoteAddress, string userAgent) { User user = GetUser(username); if (user is null) throw new DnsWebServiceException("No such user exists: " + username); if (user.Disabled) throw new DnsWebServiceException("Account is suspended."); UserSession session = new UserSession(UserSessionType.ApiToken, tokenName, user, remoteAddress, userAgent); if (!_sessions.TryAdd(session.Token, session)) throw new DnsWebServiceException("Error while creating session. Please try again."); user.LoggedInFrom(remoteAddress); return session; } public UserSession DeleteSession(string token) { if (_sessions.TryRemove(token, out UserSession session)) return session; return null; } public Permission GetPermission(PermissionSection section) { if (_permissions.TryGetValue(section, out Permission permission)) return permission; return null; } public Permission GetPermission(PermissionSection section, string subItemName) { if (_permissions.TryGetValue(section, out Permission permission)) return permission.GetSubItemPermission(subItemName); return null; } public void SetPermission(PermissionSection section, User user, PermissionFlag flags) { Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) { return new Permission(key); }); permission.SetPermission(user, flags); } public void SetPermission(PermissionSection section, string subItemName, User user, PermissionFlag flags) { Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) { return new Permission(key); }); permission.SetSubItemPermission(subItemName, user, flags); } public void SetPermission(PermissionSection section, Group group, PermissionFlag flags) { Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) { return new Permission(key); }); permission.SetPermission(group, flags); } public void SetPermission(PermissionSection section, string subItemName, Group group, PermissionFlag flags) { Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) { return new Permission(key); }); permission.SetSubItemPermission(subItemName, group, flags); } public bool RemovePermission(PermissionSection section, User user) { return _permissions.TryGetValue(section, out Permission permission) && permission.RemovePermission(user); } public bool RemovePermission(PermissionSection section, string subItemName, User user) { return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveSubItemPermission(subItemName, user); } public bool RemovePermission(PermissionSection section, Group group) { return _permissions.TryGetValue(section, out Permission permission) && permission.RemovePermission(group); } public bool RemovePermission(PermissionSection section, string subItemName, Group group) { return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveSubItemPermission(subItemName, group); } public bool RemoveAllPermissions(PermissionSection section, string subItemName) { return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveAllSubItemPermissions(subItemName); } public bool IsPermitted(PermissionSection section, User user, PermissionFlag flag) { return _permissions.TryGetValue(section, out Permission permission) && permission.IsPermitted(user, flag); } public bool IsPermitted(PermissionSection section, string subItemName, User user, PermissionFlag flag) { return _permissions.TryGetValue(section, out Permission permission) && permission.IsSubItemPermitted(subItemName, user, flag); } #endregion #region properties public ICollection Groups { get { return _groups.Values; } } public ICollection Users { get { return _users.Values; } } public ICollection Permissions { get { return _permissions.Values; } } public ICollection Sessions { get { return _sessions.Values; } } #endregion } } ================================================ FILE: DnsServerCore/Auth/Group.cs ================================================ /* 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Auth { class Group : IComparable { #region variables public const string ADMINISTRATORS = "Administrators"; public const string EVERYONE = "Everyone"; public const string DNS_ADMINISTRATORS = "DNS Administrators"; public const string DHCP_ADMINISTRATORS = "DHCP Administrators"; string _name; string _description; #endregion #region constructor public Group(string name, string description) { Name = name; Description = description; } public Group(BinaryReader bR) { switch (bR.ReadByte()) { case 1: _name = bR.ReadShortString(); _description = bR.ReadShortString(); break; default: throw new InvalidDataException("Invalid data or version not supported."); } } #endregion #region public public void WriteTo(BinaryWriter bW) { bW.Write((byte)1); bW.WriteShortString(_name); bW.WriteShortString(_description); } public override bool Equals(object obj) { if (obj is not Group other) return false; return _name.Equals(other._name, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() { return HashCode.Combine(_name); } public override string ToString() { return _name; } public int CompareTo(Group other) { return _name.CompareTo(other._name); } #endregion #region properties public string Name { get { return _name; } set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Group name cannot be null or empty.", nameof(Name)); if (value.Length > 255) throw new ArgumentException("Group name length cannot exceed 255 characters.", nameof(Name)); switch (_name?.ToLowerInvariant()) { case "everyone": case "administrators": case "dns administrators": case "dhcp administrators": throw new InvalidOperationException("Access was denied."); default: _name = value; break; } } } public string Description { get { return _description; } set { if (string.IsNullOrWhiteSpace(value)) _description = ""; else if (value.Length > 255) throw new ArgumentException("Group description length cannot exceed 255 characters.", nameof(Description)); else _description = value; } } #endregion } } ================================================ FILE: DnsServerCore/Auth/Permission.cs ================================================ /* 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 System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Auth { enum PermissionSection : byte { Unknown = 0, Dashboard = 1, Zones = 2, Cache = 3, Allowed = 4, Blocked = 5, Apps = 6, DnsClient = 7, Settings = 8, DhcpServer = 9, Administration = 10, Logs = 11 } [Flags] enum PermissionFlag : byte { None = 0, View = 1, Modify = 2, Delete = 4, ViewModify = 3, ViewModifyDelete = 7 } class Permission : IComparable { #region variables readonly PermissionSection _section; readonly string _subItemName; readonly ConcurrentDictionary _userPermissions; readonly ConcurrentDictionary _groupPermissions; readonly ConcurrentDictionary _subItemPermissions; #endregion #region constructor public Permission(PermissionSection section, string subItemName = null) { _section = section; _subItemName = subItemName; _userPermissions = new ConcurrentDictionary(1, 1); _groupPermissions = new ConcurrentDictionary(1, 1); _subItemPermissions = new ConcurrentDictionary(1, 1); } public Permission(BinaryReader bR, IReadOnlyDictionary users, IReadOnlyDictionary groups) { byte version = bR.ReadByte(); switch (version) { case 1: case 2: _section = (PermissionSection)bR.ReadByte(); { int count = bR.ReadByte(); _userPermissions = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) { string username = bR.ReadShortString().ToLowerInvariant(); PermissionFlag flag = (PermissionFlag)bR.ReadByte(); if (users.TryGetValue(username, out User user)) _userPermissions.TryAdd(user, flag); } } { int count = bR.ReadByte(); _groupPermissions = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) { string groupName = bR.ReadShortString().ToLowerInvariant(); PermissionFlag flag = (PermissionFlag)bR.ReadByte(); if (groups.TryGetValue(groupName, out Group group)) _groupPermissions.TryAdd(group, flag); } } { int count; if (version >= 2) count = bR.ReadInt32(); else count = bR.ReadByte(); _subItemPermissions = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) { string subItemName = bR.ReadShortString(); Permission subItemPermission = new Permission(bR, users, groups); _subItemPermissions.TryAdd(subItemName.ToLowerInvariant(), subItemPermission); } } break; default: throw new InvalidDataException("Invalid data or version not supported."); } } #endregion #region public public void SetPermission(User user, PermissionFlag flags) { _userPermissions[user] = flags; } public void SyncPermissions(IReadOnlyDictionary userPermissions) { //remove non-existent permissions foreach (KeyValuePair userPermission in _userPermissions) { if (!userPermissions.ContainsKey(userPermission.Key)) _userPermissions.TryRemove(userPermission.Key, out _); } //set new permissions foreach (KeyValuePair userPermission in userPermissions) _userPermissions[userPermission.Key] = userPermission.Value; } public void SetSubItemPermission(string subItemName, User user, PermissionFlag flags) { Permission subItemPermission = _subItemPermissions.GetOrAdd(subItemName.ToLowerInvariant(), delegate (string key) { return new Permission(_section, key); }); subItemPermission.SetPermission(user, flags); } public void SetPermission(Group group, PermissionFlag flags) { _groupPermissions[group] = flags; } public void SyncPermissions(IReadOnlyDictionary groupPermissions) { //remove non-existent permissions foreach (KeyValuePair groupPermission in _groupPermissions) { if (!groupPermissions.ContainsKey(groupPermission.Key)) _groupPermissions.TryRemove(groupPermission.Key, out _); } //set new permissions foreach (KeyValuePair groupPermission in groupPermissions) _groupPermissions[groupPermission.Key] = groupPermission.Value; } public void SetSubItemPermission(string subItemName, Group group, PermissionFlag flags) { Permission subItemPermission = _subItemPermissions.GetOrAdd(subItemName.ToLowerInvariant(), delegate (string key) { return new Permission(_section, key); }); subItemPermission.SetPermission(group, flags); } public bool RemovePermission(User user) { return _userPermissions.TryRemove(user, out _); } public bool RemoveSubItemPermission(string subItemName, User user) { return _subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission) && subItemPermission.RemovePermission(user); } public bool RemovePermission(Group group) { return _groupPermissions.TryRemove(group, out _); } public bool RemoveSubItemPermission(string subItemName, Group group) { return _subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission) && subItemPermission.RemovePermission(group); } public bool RemoveAllSubItemPermissions(User user) { bool removed = false; foreach (KeyValuePair subItemPermission in _subItemPermissions) { if (subItemPermission.Value.RemovePermission(user)) removed = true; } return removed; } public bool RemoveAllSubItemPermissions(Group group) { bool removed = false; foreach (KeyValuePair subItemPermission in _subItemPermissions) { if (subItemPermission.Value.RemovePermission(group)) removed = true; } return removed; } public bool RemoveAllSubItemPermissions(string subItemName) { return _subItemPermissions.TryRemove(subItemName, out _); } public Permission GetSubItemPermission(string subItemName) { if (_subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission)) return subItemPermission; return null; } public bool IsPermitted(User user, PermissionFlag flag) { if (_userPermissions.TryGetValue(user, out PermissionFlag userPermissions) && userPermissions.HasFlag(flag)) return true; foreach (Group group in user.MemberOfGroups) { if (_groupPermissions.TryGetValue(group, out PermissionFlag groupPermissions) && groupPermissions.HasFlag(flag)) return true; } return false; } public bool IsSubItemPermitted(string subItemName, User user, PermissionFlag flag) { return _subItemPermissions.TryGetValue(subItemName.ToLowerInvariant(), out Permission subItemPermission) && subItemPermission.IsPermitted(user, flag); } public void WriteTo(BinaryWriter bW) { bW.Write((byte)2); bW.Write((byte)_section); { bW.Write(Convert.ToByte(_userPermissions.Count)); foreach (KeyValuePair userPermission in _userPermissions) { bW.WriteShortString(userPermission.Key.Username); bW.Write((byte)userPermission.Value); } } { bW.Write(Convert.ToByte(_groupPermissions.Count)); foreach (KeyValuePair groupPermission in _groupPermissions) { bW.WriteShortString(groupPermission.Key.Name); bW.Write((byte)groupPermission.Value); } } { bW.Write(_subItemPermissions.Count); foreach (KeyValuePair subItemPermission in _subItemPermissions) { bW.WriteShortString(subItemPermission.Key); subItemPermission.Value.WriteTo(bW); } } } public int CompareTo(Permission other) { return _section.CompareTo(other._section); } #endregion #region properties public PermissionSection Section { get { return _section; } } public string SubItemName { get { return _subItemName; } } public IReadOnlyDictionary UserPermissions { get { return _userPermissions; } } public IReadOnlyDictionary GroupPermissions { get { return _groupPermissions; } } public IReadOnlyDictionary SubItemPermissions { get { return _subItemPermissions; } } #endregion } } ================================================ FILE: DnsServerCore/Auth/User.cs ================================================ /* 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 System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; using System.Text; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Security.OTP; namespace DnsServerCore.Auth { enum UserPasswordHashType : byte { Unknown = 0, OldScheme = 1, PBKDF2_SHA256 = 2 } class User : IComparable { #region variables public const int DEFAULT_ITERATIONS = 100000; string _displayName; string _username; UserPasswordHashType _passwordHashType; int _iterations; byte[] _salt; string _passwordHash; AuthenticatorKeyUri _totpKeyUri; bool _totpEnabled; bool _disabled; int _sessionTimeoutSeconds = 30 * 60; //default 30 mins DateTime _previousSessionLoggedOn; IPAddress _previousSessionRemoteAddress; DateTime _recentSessionLoggedOn; IPAddress _recentSessionRemoteAddress; readonly ConcurrentDictionary _memberOfGroups; #endregion #region constructor public User(string displayName, string username, string password, int iterations = DEFAULT_ITERATIONS) { Username = username; DisplayName = displayName; ChangePassword(password, iterations); _previousSessionRemoteAddress = IPAddress.Any; _recentSessionRemoteAddress = IPAddress.Any; _memberOfGroups = new ConcurrentDictionary(1, 2); } public User(BinaryReader bR, IReadOnlyDictionary groups) { int version = bR.ReadByte(); switch (version) { case 1: case 2: _displayName = bR.ReadShortString(); _username = bR.ReadShortString(); _passwordHashType = (UserPasswordHashType)bR.ReadByte(); _iterations = bR.ReadInt32(); _salt = bR.ReadBuffer(); _passwordHash = bR.ReadShortString(); if (version >= 2) { string otpKeyUri = bR.ReadString(); if (!string.IsNullOrEmpty(otpKeyUri)) _totpKeyUri = AuthenticatorKeyUri.Parse(otpKeyUri); _totpEnabled = bR.ReadBoolean(); } _disabled = bR.ReadBoolean(); _sessionTimeoutSeconds = bR.ReadInt32(); _previousSessionLoggedOn = bR.ReadDateTime(); _previousSessionRemoteAddress = IPAddressExtensions.ReadFrom(bR); _recentSessionLoggedOn = bR.ReadDateTime(); _recentSessionRemoteAddress = IPAddressExtensions.ReadFrom(bR); { int count = bR.ReadByte(); _memberOfGroups = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) { if (groups.TryGetValue(bR.ReadShortString().ToLowerInvariant(), out Group group)) _memberOfGroups.TryAdd(group.Name.ToLowerInvariant(), group); } } break; default: throw new InvalidDataException("Invalid data or version not supported."); } } #endregion #region internal internal void RenameGroup(string oldName) { if (_memberOfGroups.TryRemove(oldName.ToLowerInvariant(), out Group renamedGroup)) _memberOfGroups.TryAdd(renamedGroup.Name.ToLowerInvariant(), renamedGroup); } #endregion #region public public string GetPasswordHashFor(string password) { switch (_passwordHashType) { case UserPasswordHashType.OldScheme: using (HMAC hmac = new HMACSHA256(Encoding.UTF8.GetBytes(password))) { return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(_username))).ToLowerInvariant(); } case UserPasswordHashType.PBKDF2_SHA256: return Convert.ToHexString(Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), _salt, _iterations, HashAlgorithmName.SHA256, 32)).ToLowerInvariant(); default: throw new NotSupportedException(); } } public void ChangePassword(string newPassword, int iterations = DEFAULT_ITERATIONS) { _passwordHashType = UserPasswordHashType.PBKDF2_SHA256; _iterations = iterations; _salt = new byte[32]; RandomNumberGenerator.Fill(_salt); _passwordHash = GetPasswordHashFor(newPassword); } public void LoadOldSchemeCredentials(string passwordHash) { _passwordHashType = UserPasswordHashType.OldScheme; _passwordHash = passwordHash; } public AuthenticatorKeyUri InitializedTOTP(string issuer) { if (_totpEnabled) throw new InvalidOperationException("Time-based one-time password (TOTP) is already enabled for user: " + _username); _totpKeyUri = AuthenticatorKeyUri.Generate(issuer, _username); return _totpKeyUri; } public void EnableTOTP(string totp) { if (_totpKeyUri is null) throw new InvalidOperationException("Time-based one-time password (TOTP) was not initialized for user: " + _username); if (_totpEnabled) throw new InvalidOperationException("Time-based one-time password (TOTP) is already enabled for user: " + _username); Authenticator authenticator = new Authenticator(_totpKeyUri); if (!authenticator.IsTOTPValid(totp)) throw new Exception("Invalid time-based one-time password (TOTP) was attempted for user: " + _username); _totpEnabled = true; } public void DisableTOTP() { if (!_totpEnabled) throw new InvalidOperationException("Time-based one-time password (TOTP) is already disabled for user: " + _username); _totpKeyUri = null; _totpEnabled = false; } public void LoggedInFrom(IPAddress remoteAddress) { if (remoteAddress.IsIPv4MappedToIPv6) remoteAddress = remoteAddress.MapToIPv4(); _previousSessionLoggedOn = _recentSessionLoggedOn; _previousSessionRemoteAddress = _recentSessionRemoteAddress; _recentSessionLoggedOn = DateTime.UtcNow; _recentSessionRemoteAddress = remoteAddress; } public void AddToGroup(Group group) { if (_memberOfGroups.Count == 255) throw new InvalidOperationException("Cannot add user to group: user can be member of max 255 groups."); _memberOfGroups.TryAdd(group.Name.ToLowerInvariant(), group); } public bool RemoveFromGroup(Group group) { if (group.Name.Equals("everyone", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Access was denied."); return _memberOfGroups.TryRemove(group.Name.ToLowerInvariant(), out _); } public void SyncGroups(IReadOnlyDictionary groups) { //remove non-existent groups foreach (KeyValuePair group in _memberOfGroups) { if (!groups.ContainsKey(group.Key)) _memberOfGroups.TryRemove(group.Key, out _); } //set new groups foreach (KeyValuePair group in groups) _memberOfGroups[group.Key] = group.Value; } public bool IsMemberOfGroup(Group group) { return _memberOfGroups.ContainsKey(group.Name.ToLowerInvariant()); } public void WriteTo(BinaryWriter bW) { bW.Write((byte)2); bW.WriteShortString(_displayName); bW.WriteShortString(_username); bW.Write((byte)_passwordHashType); bW.Write(_iterations); bW.WriteBuffer(_salt); bW.WriteShortString(_passwordHash); if (_totpKeyUri is null) bW.Write(""); else bW.Write(_totpKeyUri.ToString()); bW.Write(_totpEnabled); bW.Write(_disabled); bW.Write(_sessionTimeoutSeconds); bW.Write(_previousSessionLoggedOn); IPAddressExtensions.WriteTo(_previousSessionRemoteAddress, bW); bW.Write(_recentSessionLoggedOn); IPAddressExtensions.WriteTo(_recentSessionRemoteAddress, bW); bW.Write(Convert.ToByte(_memberOfGroups.Count)); foreach (KeyValuePair group in _memberOfGroups) bW.WriteShortString(group.Value.Name.ToLowerInvariant()); } public override bool Equals(object obj) { if (obj is not User other) return false; return _username.Equals(other._username, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() { return HashCode.Combine(_username); } public override string ToString() { return _username; } public int CompareTo(User other) { return _username.CompareTo(other._username); } #endregion #region properties public string DisplayName { get { return _displayName; } set { if (string.IsNullOrWhiteSpace(value)) _displayName = _username; else if (value.Length > 255) throw new ArgumentException("Display name length cannot exceed 255 characters.", nameof(DisplayName)); else _displayName = value; } } public string Username { get { return _username; } set { if (_passwordHashType == UserPasswordHashType.OldScheme) throw new InvalidOperationException("Cannot change username when using old password hash scheme. Change password once and try again."); if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Username cannot be null or empty.", nameof(Username)); if (value.Length > 255) throw new ArgumentException("Username length cannot exceed 255 characters.", nameof(Username)); foreach (char c in value) { if ((c >= 97) && (c <= 122)) //[a-z] continue; if ((c >= 65) && (c <= 90)) //[A-Z] continue; if ((c >= 48) && (c <= 57)) //[0-9] continue; if (c == '-') continue; if (c == '_') continue; if (c == '.') continue; throw new ArgumentException("Username can contain only alpha numeric, '-', '_', or '.' characters.", nameof(Username)); } _username = value.ToLowerInvariant(); } } public UserPasswordHashType PasswordHashType { get { return _passwordHashType; } } public string PasswordHash { get { return _passwordHash; } } public AuthenticatorKeyUri TOTPKeyUri { get { return _totpKeyUri; } } public bool TOTPEnabled { get { return _totpEnabled; } } public bool Disabled { get { return _disabled; } set { _disabled = value; } } public int SessionTimeoutSeconds { get { return _sessionTimeoutSeconds; } set { if ((value < 0) || (value > 604800)) throw new ArgumentOutOfRangeException(nameof(SessionTimeoutSeconds), "Session timeout value must be between 0-604800 seconds."); if ((value > 0) && (value < 60)) value = 60; //to prevent issues with too low timeout set by mistake _sessionTimeoutSeconds = value; } } public DateTime PreviousSessionLoggedOn { get { return _previousSessionLoggedOn; } } public IPAddress PreviousSessionRemoteAddress { get { return _previousSessionRemoteAddress; } } public DateTime RecentSessionLoggedOn { get { return _recentSessionLoggedOn; } } public IPAddress RecentSessionRemoteAddress { get { return _recentSessionRemoteAddress; } } public ICollection MemberOfGroups { get { return _memberOfGroups.Values; } } #endregion } } ================================================ FILE: DnsServerCore/Auth/UserSession.cs ================================================ /* 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 System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; namespace DnsServerCore.Auth { enum UserSessionType : byte { Unknown = 0, Standard = 1, ApiToken = 2 } class UserSession : IComparable { #region variables readonly string _token; readonly UserSessionType _type; readonly string _tokenName; User _user; DateTime _lastSeen; IPAddress _lastSeenRemoteAddress; string _lastSeenUserAgent; #endregion #region constructor public UserSession(UserSessionType type, string tokenName, User user, IPAddress remoteAddress, string lastSeenUserAgent) { if ((tokenName is not null) && (tokenName.Length > 255)) throw new ArgumentOutOfRangeException(nameof(tokenName), "Token name length cannot exceed 255 characters."); if (remoteAddress.IsIPv4MappedToIPv6) remoteAddress = remoteAddress.MapToIPv4(); Span tokenBytes = stackalloc byte[32]; RandomNumberGenerator.Fill(tokenBytes); _token = Convert.ToHexString(tokenBytes).ToLowerInvariant(); _type = type; _tokenName = tokenName; _user = user; _lastSeen = DateTime.UtcNow; _lastSeenRemoteAddress = remoteAddress; _lastSeenUserAgent = lastSeenUserAgent; if ((_lastSeenUserAgent is not null) && (_lastSeenUserAgent.Length > 255)) _lastSeenUserAgent = _lastSeenUserAgent.Substring(0, 255); } public UserSession(BinaryReader bR, IReadOnlyDictionary users) { switch (bR.ReadByte()) { case 1: _token = bR.ReadShortString(); _type = (UserSessionType)bR.ReadByte(); _tokenName = bR.ReadShortString(); if (_tokenName.Length == 0) _tokenName = null; users.TryGetValue(bR.ReadShortString().ToLowerInvariant(), out _user); _lastSeen = bR.ReadDateTime(); _lastSeenRemoteAddress = IPAddressExtensions.ReadFrom(bR); _lastSeenUserAgent = bR.ReadShortString(); if (_lastSeenUserAgent.Length == 0) _lastSeenUserAgent = null; break; default: throw new InvalidDataException("Invalid data or version not supported."); } } #endregion #region public public void UpdateUserObject(IReadOnlyDictionary users) { if (users.TryGetValue(_user.Username, out User user)) _user = user; } public void UpdateLastSeen(IPAddress remoteAddress, string lastSeenUserAgent) { if (remoteAddress.IsIPv4MappedToIPv6) remoteAddress = remoteAddress.MapToIPv4(); _lastSeen = DateTime.UtcNow; _lastSeenRemoteAddress = remoteAddress; _lastSeenUserAgent = lastSeenUserAgent; if ((_lastSeenUserAgent is not null) && (_lastSeenUserAgent.Length > 255)) _lastSeenUserAgent = _lastSeenUserAgent.Substring(0, 255); } public bool HasExpired() { if (_type == UserSessionType.ApiToken) return false; if (_user.SessionTimeoutSeconds == 0) return false; return _lastSeen.AddSeconds(_user.SessionTimeoutSeconds) < DateTime.UtcNow; } public void WriteTo(BinaryWriter bW) { bW.Write((byte)1); bW.WriteShortString(_token); bW.Write((byte)_type); if (_tokenName is null) bW.Write((byte)0); else bW.WriteShortString(_tokenName); bW.WriteShortString(_user.Username); bW.Write(_lastSeen); _lastSeenRemoteAddress.WriteTo(bW); if (_lastSeenUserAgent is null) bW.Write((byte)0); else bW.WriteShortString(_lastSeenUserAgent); } public int CompareTo(UserSession other) { return other._lastSeen.CompareTo(_lastSeen); } #endregion #region properties public string Token { get { return _token; } } public UserSessionType Type { get { return _type; } } public string TokenName { get { return _tokenName; } } public User User { get { return _user; } } public DateTime LastSeen { get { return _lastSeen; } } public IPAddress LastSeenRemoteAddress { get { return _lastSeenRemoteAddress; } } public string LastSeenUserAgent { get { return _lastSeenUserAgent; } } #endregion } } ================================================ FILE: DnsServerCore/Cluster/ClusterManager.cs ================================================ /* 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.Dns; using DnsServerCore.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Zones; using DnsServerCore.HttpApi; using DnsServerCore.HttpApi.Models; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Cluster { sealed class ClusterManager : IDisposable { #region variables const ushort HEARTBEAT_REFRESH_INTERVAL_SECONDS = 30; const ushort HEARTBEAT_RETRY_INTERVAL_SECONDS = 10; const ushort CONFIG_REFRESH_INTERVAL_SECONDS = 900; const ushort CONFIG_RETRY_INTERVAL_SECONDS = 60; readonly DnsWebService _dnsWebService; string _clusterDomain; ushort _heartbeatRefreshIntervalSeconds; ushort _heartbeatRetryIntervalSeconds; ushort _configRefreshIntervalSeconds; ushort _configRetryIntervalSeconds; DateTime _configLastSynced; IReadOnlyDictionary _clusterNodes; readonly SemaphoreSlim _configRefreshLock = new SemaphoreSlim(1, 1); readonly Timer _configRefreshTimer; bool _configRefreshTimerTriggered; IReadOnlyCollection _configRefreshIncludeZones; const int CONFIG_REFRESH_TIMER_INTERVAL = 5000; readonly Timer _notifyAllSecondaryNodesTimer; bool _notifyAllSecondaryNodesTimerTriggered; const int NOTIFY_ALL_SECONDARY_NODES_TIMER_INTERVAL = 5000; readonly Timer _clusterUpdateForSecondaryNodeChangesTimer; bool _clusterUpdateForSecondaryNodeChangesTimerTriggered; const int CLUSTER_UPDATE_FOR_SECONDARY_NODE_CHANGES_TIMER_INTERVAL = 5000; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; volatile int _recordUpdateForMemberZonesId; #endregion #region constructor public ClusterManager(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; _configRefreshTimer = new Timer(ConfigRefreshTimerCallbackAsync); _notifyAllSecondaryNodesTimer = new Timer(NotifyAllSecondaryNodesTimerCallbackAsync); _clusterUpdateForSecondaryNodeChangesTimer = new Timer(ClusterUpdateForSecondaryNodeChangesTimerCallbackAsync); _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { if (_pendingSave) { try { SaveConfigFileInternal(); _pendingSave = false; } catch (Exception ex) { _dnsWebService.LogManager.Write(ex); //set timer to retry again _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _configRefreshTimer?.Dispose(); _notifyAllSecondaryNodesTimer?.Dispose(); _clusterUpdateForSecondaryNodeChangesTimer?.Dispose(); DisposeAllNodes(); lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveConfigFileInternal(); } catch (Exception ex) { _dnsWebService.LogManager.Write(ex); } finally { _pendingSave = false; } } } _configRefreshLock?.Dispose(); _disposed = true; } #endregion #region config public void LoadConfigFile() { string configFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.config"); try { DisposeAllNodes(); //dispose existing nodes, if any using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS); } InitializeHeartbeatTimerFor(_clusterNodes); UpdateConfigRefreshTimer(); _dnsWebService.LogManager.Write("DNS Server Cluster config file was loaded: " + configFile); } catch (FileNotFoundException) { //do nothing } catch (Exception ex) { _dnsWebService.LogManager.Write("DNS Server encountered an error while loading the Cluster config file: " + configFile + "\r\n" + ex.ToString()); } } public void LoadConfig(Stream s) { lock (_saveLock) { DisposeAllNodes(); //dispose existing nodes, if any ReadConfigFrom(s); InitializeHeartbeatTimerFor(_clusterNodes); UpdateConfigRefreshTimer(); //save config file SaveConfigFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void UpdateConfigRefreshTimer(int refreshInterval = CONFIG_REFRESH_TIMER_INTERVAL) { //ensure that the new refresh interval is applied using lock _configRefreshLock.Wait(); try { if (ClusterInitialized && (GetSelfNode().Type == ClusterNodeType.Secondary)) _configRefreshTimer.Change(refreshInterval, Timeout.Infinite); //start config refresh timer only for secondary nodes else _configRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite); } finally { _configRefreshLock.Release(); } } private void StopConfigRefreshTimer() { //ensure that the timer is stopped using lock _configRefreshLock.Wait(); try { _configRefreshTimer.Change(Timeout.Infinite, Timeout.Infinite); } finally { _configRefreshLock.Release(); } } private void SaveConfigFileInternal() { if (!ClusterInitialized) throw new InvalidOperationException(); string configFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.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); } } _dnsWebService.LogManager.Write("DNS Server Cluster config file was saved: " + configFile); } public void SaveConfigFile() { if (!ClusterInitialized) throw new InvalidOperationException(); lock (_saveLock) { if (_pendingSave) return; _pendingSave = true; _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void UnloadAndDeleteConfigFile() { StopConfigRefreshTimer(); DisposeAllNodes(); //dispose existing nodes, if any lock (_saveLock) { //unload _clusterDomain = null; _configLastSynced = default; _clusterNodes = null; //delete config file string configFile = Path.Combine(_dnsWebService.ConfigFolder, "cluster.config"); try { if (File.Exists(configFile)) { File.Delete(configFile); _dnsWebService.LogManager.Write("DNS Server Cluster config file was deleted: " + configFile); } } catch (Exception ex) { _dnsWebService.LogManager.Write("DNS Server encountered an error while deleting the Cluster config file: " + configFile + "\r\n" + ex.ToString()); } if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void ReadConfigFrom(Stream s) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "CL") //format throw new InvalidDataException("DNS Server Cluster config file format is invalid."); int version = bR.ReadByte(); switch (version) { case 1: _clusterDomain = bR.ReadString(); _heartbeatRefreshIntervalSeconds = bR.ReadUInt16(); _heartbeatRetryIntervalSeconds = bR.ReadUInt16(); _configRefreshIntervalSeconds = bR.ReadUInt16(); _configRetryIntervalSeconds = bR.ReadUInt16(); _configLastSynced = bR.ReadDateTime(); Dictionary clusterNodes = null; int count = bR.ReadByte(); if (count > 0) { clusterNodes = new Dictionary(count); for (int i = 0; i < count; i++) { ClusterNode node = new ClusterNode(this, bR); clusterNodes.TryAdd(node.Id, node); } } _clusterNodes = clusterNodes; break; default: throw new InvalidDataException("DNS Server Cluster config version not supported."); } } private void WriteConfigTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("CL")); //format bW.Write((byte)1); //version bW.Write(_clusterDomain); bW.Write(_heartbeatRefreshIntervalSeconds); bW.Write(_heartbeatRetryIntervalSeconds); bW.Write(_configRefreshIntervalSeconds); bW.Write(_configRetryIntervalSeconds); bW.Write(_configLastSynced); IReadOnlyDictionary clusterNodes = _clusterNodes; if (clusterNodes is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(clusterNodes.Count)); foreach (KeyValuePair node in clusterNodes) node.Value.WriteTo(bW); } } #endregion #region private private void DisposeAllNodes() { IReadOnlyDictionary clusterNodes = _clusterNodes; if (clusterNodes is not null) { foreach (KeyValuePair clusterNode in clusterNodes) clusterNode.Value.Dispose(); } } private static void InitializeHeartbeatTimerFor(IReadOnlyDictionary clusterNodes) { //start heartbeat timers for all nodes except self node foreach (KeyValuePair node in clusterNodes) { if (node.Value.State == ClusterNodeState.Self) continue; node.Value.InitializeHeartbeatTimer(); } } private void UpdateHeartbeatTimerForAllClusterNodes() { IReadOnlyDictionary clusterNodes = _clusterNodes; if (clusterNodes is not null) { foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.State == ClusterNodeState.Self) continue; clusterNode.Value.UpdateHeartbeatTimer(); } } } private void DeleteAllClusterConfig() { //delete cluster catalog zone string clusterCatalogDomain = "cluster-catalog." + _clusterDomain; AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain); if (clusterCatalogZoneInfo is not null) { if (_dnsWebService.DnsServer.AuthZoneManager.DeleteZone(clusterCatalogZoneInfo, true)) _dnsWebService.AuthManager.RemoveAllPermissions(PermissionSection.Zones, clusterCatalogDomain); } //remove TSIG key for cluster catalog zone { IReadOnlyDictionary existingKeys = _dnsWebService.DnsServer.TsigKeys; if (existingKeys is not null) { Dictionary updatedKeys = new Dictionary(existingKeys); updatedKeys.Remove(clusterCatalogDomain); _dnsWebService.DnsServer.TsigKeys = updatedKeys; } } //delete cluster API token { foreach (UserSession session in _dnsWebService.AuthManager.Sessions) { if ((session.Type == UserSessionType.ApiToken) && (session.TokenName == _clusterDomain)) _dnsWebService.AuthManager.DeleteSession(session.Token); } } //finalize if (_dnsWebService.DnsServer.ServerDomain.EndsWith("." + _clusterDomain, StringComparison.OrdinalIgnoreCase)) _dnsWebService.DnsServer.ServerDomain = _dnsWebService.DnsServer.ServerDomain.Substring(0, _dnsWebService.DnsServer.ServerDomain.Length - (_clusterDomain.Length + 1)); //save all changes _dnsWebService.DnsServer.SaveConfigFile(); _dnsWebService.AuthManager.SaveConfigFile(); UnloadAndDeleteConfigFile(); } #endregion #region primary node public void InitializeCluster(string clusterDomain, IReadOnlyList primaryNodeIpAddresses, UserSession session) { if (ClusterInitialized) throw new DnsServerException("Failed to initialize Cluster: the Cluster is already initialized."); if (!_dnsWebService.IsWebServiceTlsEnabled) throw new InvalidOperationException(); clusterDomain = clusterDomain.ToLowerInvariant(); //create self node string serverDomain = _dnsWebService.DnsServer.ServerDomain; if (!serverDomain.EndsWith("." + clusterDomain, StringComparison.OrdinalIgnoreCase)) { int x = serverDomain.IndexOf('.'); if (x < 0) serverDomain = serverDomain + "." + clusterDomain; else serverDomain = string.Concat(serverDomain.AsSpan(0, x), ".", clusterDomain); } Uri primaryNodeUrl = new Uri($"https://{serverDomain}:{_dnsWebService.WebServiceTlsPort}/"); ClusterNode selfPrimaryNode = new ClusterNode(this, RandomNumberGenerator.GetInt32(int.MaxValue), primaryNodeUrl, primaryNodeIpAddresses, ClusterNodeType.Primary, ClusterNodeState.Self); //create cluster primary zone AuthZoneInfo clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterDomain); if (clusterZoneInfo is null) { clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreatePrimaryZone(clusterDomain); if (clusterZoneInfo is null) throw new DnsServerException($"Failed to initialize Cluster: failed to create the Cluster zone '{clusterDomain}'. Please try again."); } else if (clusterZoneInfo.Type != AuthZoneType.Primary) { throw new DnsServerException($"Failed to initialize Cluster: the zone '{clusterZoneInfo.Name}' already exists and is not a Primary zone. Please delete the existing zone or use a different Cluster domain name."); } //create cluster catalog zone string clusterCatalogDomain = "cluster-catalog." + clusterDomain; AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain); if (clusterCatalogZoneInfo is null) { clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateCatalogZone(clusterCatalogDomain); if (clusterCatalogZoneInfo is null) throw new DnsServerException($"Failed to initialize Cluster: failed to create the Cluster Catalog zone '{clusterCatalogDomain}'. Please try again."); } else if (clusterCatalogZoneInfo.Type != AuthZoneType.Catalog) { throw new DnsServerException($"Failed to initialize Cluster: the zone '{clusterCatalogZoneInfo.Name}' already exists and is not a Catalog zone. Please delete the existing zone or use a different Cluster domain name."); } //set cluster primary zone permissions _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View); //set cluster catalog zone permissions _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View); //ensure cluster zone is a member of cluster catalog zone if (clusterZoneInfo.CatalogZoneName is null) _dnsWebService.DnsServer.AuthZoneManager.AddCatalogMemberZone(clusterCatalogZoneInfo.Name, clusterZoneInfo); else if (!clusterZoneInfo.CatalogZoneName.Equals(clusterCatalogZoneInfo.Name, StringComparison.OrdinalIgnoreCase)) _dnsWebService.DnsServer.AuthZoneManager.ChangeCatalogMemberZoneOwnership(clusterZoneInfo, clusterCatalogZoneInfo.Name); //sign cluster zone if (clusterZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned) { DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.KeySigningKey); DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.ZoneSigningKey); zskPrivateKey.RolloverDays = 90; _dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZone(clusterZoneInfo.Name, kskPrivateKey, zskPrivateKey, 3600, false); } //create TSIG key for cluster catalog zone if it does not exist { IReadOnlyDictionary existingKeys = _dnsWebService.DnsServer.TsigKeys; if (existingKeys is null) { Dictionary updatedKeys = new Dictionary(); updatedKeys[clusterCatalogDomain] = new TsigKey(clusterCatalogDomain, TsigAlgorithm.HMAC_SHA256); _dnsWebService.DnsServer.TsigKeys = updatedKeys; } else if (!existingKeys.ContainsKey(clusterCatalogDomain)) { Dictionary updatedKeys = new Dictionary(existingKeys); updatedKeys[clusterCatalogDomain] = new TsigKey(clusterCatalogDomain, TsigAlgorithm.HMAC_SHA256); _dnsWebService.DnsServer.TsigKeys = updatedKeys; } } //create cluster API token if it does not exist { List userSessions = _dnsWebService.AuthManager.GetSessions(session.User); bool apiTokenExists = false; foreach (UserSession existingSession in userSessions) { if ((existingSession.Type == UserSessionType.ApiToken) && (existingSession.TokenName == clusterZoneInfo.Name)) { apiTokenExists = true; break; } } if (!apiTokenExists) _dnsWebService.AuthManager.CreateApiToken(clusterZoneInfo.Name, session.User.Username, session.LastSeenRemoteAddress, session.LastSeenUserAgent); } //dispose existing nodes, if any DisposeAllNodes(); //initialize cluster _clusterNodes = new Dictionary(1) { [selfPrimaryNode.Id] = selfPrimaryNode }; _clusterDomain = clusterZoneInfo.Name; _heartbeatRefreshIntervalSeconds = HEARTBEAT_REFRESH_INTERVAL_SECONDS; _heartbeatRetryIntervalSeconds = HEARTBEAT_RETRY_INTERVAL_SECONDS; _configRefreshIntervalSeconds = CONFIG_REFRESH_INTERVAL_SECONDS; _configRetryIntervalSeconds = CONFIG_RETRY_INTERVAL_SECONDS; //update cluster primary zone and save zone file FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values RemoveAllClusterPrimaryZoneNSRecords(); //remove all existing NS records AddClusterPrimaryZoneRecordsFor(selfPrimaryNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate); //update cluster catalog zone ACLs, TSIG key name and save zone file UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo); //finalize _dnsWebService.DnsServer.ServerDomain = selfPrimaryNode.Name; //save all changes _dnsWebService.DnsServer.SaveConfigFile(); _dnsWebService.AuthManager.SaveConfigFile(); SaveConfigFile(); } public void DeleteCluster(bool forceDelete) { if (!ClusterInitialized) throw new DnsServerException("Failed to delete Cluster: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to delete Cluster: only a Primary node can delete the Cluster."); if (!forceDelete && (_clusterNodes.Count > 1)) throw new DnsServerException("Failed to delete Cluster: please remove all Secondary nodes before deleting the Cluster."); DeleteAllClusterConfig(); } public ClusterNode JoinCluster(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate) { if (!ClusterInitialized) throw new DnsServerException("Failed to add Secondary node: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to add Secondary node: only a Primary node can add a Secondary node to the Cluster."); string secondaryNodeDomain = secondaryNodeUrl.Host.ToLowerInvariant(); if (!secondaryNodeDomain.EndsWith("." + _clusterDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("Failed to add Secondary node: the Secondary node domain name must be a subdomain of the Cluster domain name."); IReadOnlyDictionary existingClusterNodes = _clusterNodes; //validate for duplicate names foreach (KeyValuePair existingClusterNode in existingClusterNodes) { if (existingClusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("Failed to add Secondary node: the Secondary node's domain name already exists in the Cluster. Please try again after changing the Secondary DNS Server's domain name."); } //add secondary node to cluster nodes ClusterNode secondaryNode = new ClusterNode(this, secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Unknown); Dictionary updatedClusterNodes = new Dictionary(existingClusterNodes.Count + 1); foreach (KeyValuePair existingClusterNode in existingClusterNodes) updatedClusterNodes[existingClusterNode.Value.Id] = existingClusterNode.Value; if (!updatedClusterNodes.TryAdd(secondaryNode.Id, secondaryNode)) throw new DnsServerException("Failed to add Secondary node: node ID already exists in the Cluster. Please try again."); if (updatedClusterNodes.Count > 255) throw new DnsServerException("Failed to add Secondary node: a maximum of 255 nodes are supported by the Cluster."); IReadOnlyDictionary originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes); if (!ReferenceEquals(originalValue, existingClusterNodes)) throw new DnsServerException("Failed to add Secondary node: please try again."); secondaryNode.InitializeHeartbeatTimer(); //update cluster zone and save zone file FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate); //update cluster catalog zone ACLs and save zone file UpdateClusterCatalogZoneOptions(); //save all changes SaveConfigFile(); //notify all secondary nodes TriggerNotifyAllSecondaryNodes(); //trigger NS and SOA update for member zones TriggerRecordUpdateForClusterCatalogMemberZones(); return secondaryNode; } public async Task AskSecondaryNodeToLeaveClusterAsync(int secondaryNodeId) { if (!ClusterInitialized) throw new DnsServerException("Failed to ask Secondary node to leave: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to ask Secondary node to leave: only a Primary node can ask a Secondary node to leave the Cluster."); //find existing secondary node if (!_clusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode)) throw new DnsServerException("Failed to ask Secondary node to leave: the specified node does not exist in the Cluster."); if (secondaryNode.Type == ClusterNodeType.Primary) throw new DnsServerException("Failed to ask Secondary node to leave: the specified node is the Cluster Primary node and cannot be removed."); //ask secondary node to leave the cluster await secondaryNode.AskSecondaryNodeToLeaveClusterAsync(); return secondaryNode; } public ClusterNode DeleteSecondaryNode(int secondaryNodeId) { if (!ClusterInitialized) throw new DnsServerException("Failed to delete Secondary node: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to delete Secondary node: only a Primary node can delete a Secondary node from the Cluster."); //find existing secondary node IReadOnlyDictionary existingClusterNodes = _clusterNodes; if (!existingClusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode)) throw new DnsServerException("Failed to delete Secondary node: the specified node does not exist in the Cluster."); if (secondaryNode.Type == ClusterNodeType.Primary) throw new DnsServerException("Failed to delete Secondary node: the specified node is the Cluster Primary node and cannot be deleted."); //delete secondary node from cluster nodes Dictionary updatedClusterNodes = new Dictionary(existingClusterNodes.Count - 1); foreach (KeyValuePair existingClusterNode in existingClusterNodes) { if (existingClusterNode.Key == secondaryNodeId) continue; updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value; } IReadOnlyDictionary originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes); if (!ReferenceEquals(originalValue, existingClusterNodes)) throw new InvalidOperationException("Failed to delete Secondary node: please try again."); secondaryNode.Dispose(); //update cluster zone and save zone file RemoveClusterPrimaryZoneRecordsFor(secondaryNode); //update cluster catalog zone ACLs and save zone file UpdateClusterCatalogZoneOptions(); //save all changes SaveConfigFile(); //notify all secondary nodes TriggerNotifyAllSecondaryNodes(); //trigger NS and SOA update for member zones TriggerRecordUpdateForClusterCatalogMemberZones(); return secondaryNode; } public ClusterNode UpdateSecondaryNode(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyList secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate) { if (!ClusterInitialized) throw new DnsServerException("Failed to update Secondary node: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to update Secondary node: only a Primary node can update a Secondary node's details in the Cluster."); IReadOnlyDictionary clusterNodes = _clusterNodes; if (!clusterNodes.TryGetValue(secondaryNodeId, out ClusterNode secondaryNode)) throw new DnsServerException("Failed to update Secondary node: the specified node does not exist in the Cluster."); if (secondaryNode.Type != ClusterNodeType.Secondary) throw new DnsServerException("Failed to update Secondary node: the specified node to update must be a Secondary node."); //validate for duplicate names foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Key == secondaryNodeId) continue; //skip self if (clusterNode.Value.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("Failed to update Secondary node: the Secondary node's domain name already exists in the Cluster. Please try again after changing the Secondary DNS Server's domain name."); } bool secondaryNodeDomainChanged = !secondaryNode.Name.Equals(secondaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase); //find existing record TTL values FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //update cluster zone to remove existing records for secondary node RemoveClusterPrimaryZoneRecordsFor(secondaryNode); //update secondary node URL and IP address secondaryNode.UpdateNode(secondaryNodeUrl, secondaryNodeIpAddresses); //update cluster zone to add updated records for secondary node and save zone file AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate); //update cluster catalog zone ACLs and save zone file UpdateClusterCatalogZoneOptions(); //save all changes SaveConfigFile(); //notify all secondary nodes TriggerNotifyAllSecondaryNodes(); //trigger NS and SOA update for member zones only if secondary node domain name has changed if (secondaryNodeDomainChanged) TriggerRecordUpdateForClusterCatalogMemberZones(); return secondaryNode; } public Task TransferConfigAsync(Stream zipStream, DateTime ifModifiedSince, IReadOnlyCollection includeZones) { if (!ClusterInitialized) throw new DnsServerException("Failed to transfer configuration: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to transfer configuration: only the Primary node can transfer the configuration."); return _dnsWebService.BackupConfigAsync(zipStream: zipStream, authConfig: true, clusterConfig: false, webServiceSettings: false, dnsSettings: true, logSettings: false, zones: true, allowedZones: true, blockedZones: true, blockLists: true, apps: true, scopes: false, stats: false, logs: false, isConfigTransfer: true, ifModifiedSince: ifModifiedSince, includeZones: includeZones); } public void UpdateClusterOptions(ushort heartbeatRefreshIntervalSeconds, ushort heartbeatRetryIntervalSeconds, ushort configRefreshIntervalSeconds, ushort configRetryIntervalSeconds) { if (!ClusterInitialized) throw new DnsServerException("Failed to update Cluster options: the Cluster is not initialized."); if (GetSelfNode().Type != ClusterNodeType.Primary) throw new DnsServerException("Failed to update Cluster options: only the Primary node can update the Cluster options."); if ((heartbeatRefreshIntervalSeconds < 10) || (heartbeatRefreshIntervalSeconds > 300)) throw new ArgumentOutOfRangeException(nameof(heartbeatRefreshIntervalSeconds)); if ((heartbeatRetryIntervalSeconds < 10) || (heartbeatRetryIntervalSeconds > 300)) throw new ArgumentOutOfRangeException(nameof(heartbeatRetryIntervalSeconds)); if ((configRefreshIntervalSeconds < 30) || (configRefreshIntervalSeconds > 3600)) throw new ArgumentOutOfRangeException(nameof(configRefreshIntervalSeconds)); if ((configRetryIntervalSeconds < 30) || (configRetryIntervalSeconds > 3600)) throw new ArgumentOutOfRangeException(nameof(configRetryIntervalSeconds)); if (configRefreshIntervalSeconds <= heartbeatRefreshIntervalSeconds) throw new ArgumentException("Failed to update Cluster options: The config refresh interval must be greater than the heartbeat refresh interval."); bool changed = false; if (_heartbeatRefreshIntervalSeconds != heartbeatRefreshIntervalSeconds) { _heartbeatRefreshIntervalSeconds = heartbeatRefreshIntervalSeconds; changed = true; } if (_heartbeatRetryIntervalSeconds != heartbeatRetryIntervalSeconds) { _heartbeatRetryIntervalSeconds = heartbeatRetryIntervalSeconds; changed = true; } if (_configRefreshIntervalSeconds != configRefreshIntervalSeconds) { _configRefreshIntervalSeconds = configRefreshIntervalSeconds; changed = true; } if (_configRetryIntervalSeconds != configRetryIntervalSeconds) { _configRetryIntervalSeconds = configRetryIntervalSeconds; changed = true; } if (changed) { //apply new interval to all cluster nodes immediately UpdateHeartbeatTimerForAllClusterNodes(); //save changes SaveConfigFile(); //trigger notify to all secondary nodes TriggerNotifyAllSecondaryNodes(); } } private void FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl) { //try get existing NS record TTL IReadOnlyList existingNSRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS); if (existingNSRecords.Count > 0) { DnsResourceRecord existingNSRecord = existingNSRecords[0]; nsTtl = existingNSRecord.TTL; string nsDomain = (existingNSRecord.RDATA as DnsNSRecordData).NameServer; if (nsDomain.EndsWith("." + _clusterDomain, StringComparison.OrdinalIgnoreCase)) { IReadOnlyList existingRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, nsDomain, DnsResourceRecordType.A); if (existingRecords.Count == 0) existingRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, nsDomain, DnsResourceRecordType.AAAA); if (existingRecords.Count > 0) aTtl = existingRecords[0].TTL; else aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl; } else { aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl; } } else { nsTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultNsRecordTtl; aTtl = _dnsWebService.DnsServer.AuthZoneManager.DefaultRecordTtl; } } private void RemoveAllClusterPrimaryZoneNSRecords() { //remove all existing NS records _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS); } private void RemoveClusterPrimaryZoneRecordsFor(ClusterNode node) { //remove NS record _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(_clusterDomain, _clusterDomain, DnsResourceRecordType.NS, new DnsNSRecordData(node.Name)); //remove A/AAAA records _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.A); _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, node.Name, DnsResourceRecordType.AAAA); //remove PTR record foreach (IPAddress ipAddress in node.IPAddresses) { string ptrDomain = Zone.GetReverseZone(ipAddress, ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); _dnsWebService.DnsServer.AuthZoneManager.DeleteRecord(ptrDomain, ptrDomain, DnsResourceRecordType.PTR, new DnsPTRRecordData(node.Name)); } //remove TLSA DANE-EE record _dnsWebService.DnsServer.AuthZoneManager.DeleteRecords(_clusterDomain, $"_{node.Url.Port}._tcp.{node.Name}", DnsResourceRecordType.TLSA); //save zone file _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(_clusterDomain); } private void AddClusterPrimaryZoneRecordsFor(ClusterNode node, uint nsTtl, uint aTtl, X509Certificate2 certificate) { const string recordComments = "Cluster managed record. Do not update or delete."; if (node.Type == ClusterNodeType.Primary) { //update SOA record IReadOnlyList existingSoaRecords = _dnsWebService.DnsServer.AuthZoneManager.GetRecords(_clusterDomain, _clusterDomain, DnsResourceRecordType.SOA); DnsResourceRecord existingSoaRecord = existingSoaRecords[0]; DnsSOARecordData existingSoa = existingSoaRecord.RDATA as DnsSOARecordData; DnsSOARecordData newSoa = new DnsSOARecordData(node.Name, existingSoa.ResponsiblePerson, existingSoa.Serial, existingSoa.Refresh, existingSoa.Retry, existingSoa.Expire, existingSoa.Minimum); DnsResourceRecord newSoaRecord = new DnsResourceRecord(_clusterDomain, DnsResourceRecordType.SOA, DnsClass.IN, existingSoaRecord.TTL, newSoa); _dnsWebService.DnsServer.AuthZoneManager.SetRecord(_clusterDomain, newSoaRecord); } //add NS record DnsResourceRecord nsRecord = new DnsResourceRecord(_clusterDomain, DnsResourceRecordType.NS, DnsClass.IN, nsTtl, new DnsNSRecordData(node.Name)); GenericRecordInfo nsRecordInfo = nsRecord.GetAuthGenericRecordInfo(); nsRecordInfo.LastModified = DateTime.UtcNow; nsRecordInfo.Comments = recordComments; _dnsWebService.DnsServer.AuthZoneManager.AddRecord(_clusterDomain, nsRecord); //set A/AAAA record List ipv4AddressRecords = new List(node.IPAddresses.Count); List ipv6AddressRecords = new List(node.IPAddresses.Count); foreach (IPAddress ipAddress in node.IPAddresses) { DnsResourceRecord record; switch (ipAddress.AddressFamily) { case AddressFamily.InterNetwork: record = new DnsResourceRecord(node.Name, DnsResourceRecordType.A, DnsClass.IN, aTtl, new DnsARecordData(ipAddress)); ipv4AddressRecords.Add(record); break; case AddressFamily.InterNetworkV6: record = new DnsResourceRecord(node.Name, DnsResourceRecordType.AAAA, DnsClass.IN, aTtl, new DnsAAAARecordData(ipAddress)); ipv6AddressRecords.Add(record); break; default: throw new InvalidOperationException(); } GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.Comments = recordComments; } if (ipv4AddressRecords.Count > 0) _dnsWebService.DnsServer.AuthZoneManager.SetRecords(_clusterDomain, ipv4AddressRecords); if (ipv6AddressRecords.Count > 0) _dnsWebService.DnsServer.AuthZoneManager.SetRecords(_clusterDomain, ipv6AddressRecords); //set PTR record foreach (IPAddress ipAddress in node.IPAddresses) { string ptrDomain = Zone.GetReverseZone(ipAddress, ipAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); AuthZoneInfo reverseZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain); if (reverseZoneInfo is not null) { if (!reverseZoneInfo.Internal && (reverseZoneInfo.Type == AuthZoneType.Primary)) { DnsResourceRecord ptrRecord = new DnsResourceRecord(ptrDomain, DnsResourceRecordType.PTR, DnsClass.IN, aTtl, new DnsPTRRecordData(node.Name)); GenericRecordInfo ptrRecordInfo = ptrRecord.GetAuthGenericRecordInfo(); ptrRecordInfo.LastModified = DateTime.UtcNow; ptrRecordInfo.Comments = recordComments; _dnsWebService.DnsServer.AuthZoneManager.SetRecord(reverseZoneInfo.Name, ptrRecord); } } } //set TLSA DANE-EE record DnsResourceRecord tlsaRecord = new DnsResourceRecord($"_{node.Url.Port}._tcp.{node.Name}", DnsResourceRecordType.TLSA, DnsClass.IN, aTtl, new DnsTLSARecordData(DnsTLSACertificateUsage.DANE_EE, DnsTLSASelector.SPKI, DnsTLSAMatchingType.SHA2_256, certificate)); GenericRecordInfo tlsaRecordInfo = tlsaRecord.GetAuthGenericRecordInfo(); tlsaRecordInfo.LastModified = DateTime.UtcNow; tlsaRecordInfo.Comments = recordComments; _dnsWebService.DnsServer.AuthZoneManager.SetRecord(_clusterDomain, tlsaRecord); //save zone file _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(_clusterDomain); } public void UpdateClusterRecordsFor(AuthZoneInfo zoneInfo) { if (zoneInfo.Type != AuthZoneType.Primary) throw new InvalidOperationException(); //set NS records for cluster IReadOnlyList existingNSRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.NS); uint ttl; if (existingNSRecords.Count > 0) ttl = existingNSRecords[0].TTL; else ttl = _dnsWebService.DnsServer.AuthZoneManager.DefaultNsRecordTtl; IReadOnlyDictionary clusterNodes = _clusterNodes; DnsResourceRecord[] nsRecords = new DnsResourceRecord[clusterNodes.Count]; int i = 0; foreach (KeyValuePair clusterNode in clusterNodes) nsRecords[i++] = new DnsResourceRecord(zoneInfo.Name, DnsResourceRecordType.NS, DnsClass.IN, ttl, new DnsNSRecordData(clusterNode.Value.Name)); //set NS record _dnsWebService.DnsServer.AuthZoneManager.SetRecords(zoneInfo.Name, nsRecords); //ensure correct SOA primary name server IReadOnlyList existingSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (existingSoaRecords.Count > 0) { DnsResourceRecord existingSoaRecord = existingSoaRecords[0]; DnsSOARecordData existingSoa = existingSoaRecord.RDATA as DnsSOARecordData; //set SOA record _dnsWebService.DnsServer.AuthZoneManager.SetRecords(zoneInfo.Name, [new DnsResourceRecord(zoneInfo.Name, DnsResourceRecordType.SOA, DnsClass.IN, existingSoaRecord.TTL, new DnsSOARecordData(_dnsWebService.DnsServer.ServerDomain, existingSoa.ResponsiblePerson, existingSoa.Serial, existingSoa.Refresh, existingSoa.Retry, existingSoa.Expire, existingSoa.Minimum))]); } } private void TriggerRecordUpdateForClusterCatalogMemberZones() { int id = RandomNumberGenerator.GetInt32(int.MaxValue); _recordUpdateForMemberZonesId = id; ThreadPool.QueueUserWorkItem(delegate (object state) { try { //get cluster catalog zone info AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo("cluster-catalog." + _clusterDomain); if ((clusterCatalogZoneInfo is null) || (clusterCatalogZoneInfo.Type != AuthZoneType.Catalog)) throw new InvalidOperationException(); //get all member zone names for cluster catalog zone IReadOnlyCollection memberZoneNames = (clusterCatalogZoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames(); foreach (string memberZoneName in memberZoneNames) { if (_recordUpdateForMemberZonesId != id) return; //stop current update since another update has been triggered //get member zone info AuthZoneInfo memberZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(memberZoneName); if ((memberZoneInfo is null) || (memberZoneInfo.Type != AuthZoneType.Primary)) continue; //process is only for primary zones //update NS and SOA records for the member zone UpdateClusterRecordsFor(memberZoneInfo); //save zone file _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(memberZoneName); } _dnsWebService.LogManager.Write("The Cluster Catalog member zones NS and SOA records were successfully updated to reflect the Cluster changes."); } catch (Exception ex) { _dnsWebService.LogManager.Write(ex); } }); } private void UpdateClusterCatalogZoneOptions() { string clusterCatalogDomain = "cluster-catalog." + _clusterDomain; AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain); if (clusterCatalogZoneInfo is null) throw new InvalidOperationException(); UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo); } private void UpdateClusterCatalogZoneOptions(AuthZoneInfo clusterCatalogZoneInfo) { //set cluster catalog zone options for Zone Transfer ACLs and notify addresses IReadOnlyDictionary clusterNodes = _clusterNodes; List zoneTransferACL = new List(clusterNodes.Count * 2); List notifyNameServers = new List(clusterNodes.Count * 2); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.Type == ClusterNodeType.Primary) continue; foreach (IPAddress ipAddress in clusterNode.Value.IPAddresses) zoneTransferACL.Add(new NetworkAccessControl(ipAddress, 32)); notifyNameServers.AddRange(clusterNode.Value.IPAddresses); } clusterCatalogZoneInfo.ZoneTransferNetworkACL = zoneTransferACL; clusterCatalogZoneInfo.NotifyNameServers = notifyNameServers; clusterCatalogZoneInfo.ZoneTransfer = AuthZoneTransfer.UseSpecifiedNetworkACL; clusterCatalogZoneInfo.Notify = AuthZoneNotify.SpecifiedNameServers; //set cluster catalog zone options for zone transfer TSIG key names IReadOnlySet existingKeyNames = clusterCatalogZoneInfo.ZoneTransferTsigKeyNames; if (existingKeyNames is null) { HashSet updatedKeyNames = [clusterCatalogZoneInfo.Name]; clusterCatalogZoneInfo.ZoneTransferTsigKeyNames = updatedKeyNames; } else if (!existingKeyNames.Contains(clusterCatalogZoneInfo.Name)) { HashSet updatedKeyNames = [.. existingKeyNames, clusterCatalogZoneInfo.Name]; clusterCatalogZoneInfo.ZoneTransferTsigKeyNames = updatedKeyNames; } _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(clusterCatalogZoneInfo.Name); } public void TriggerNotifyAllSecondaryNodesIfPrimarySelfNode() { if (GetSelfNode().Type == ClusterNodeType.Primary) TriggerNotifyAllSecondaryNodes(); } public void TriggerNotifyAllSecondaryNodes(int notifyInterval = NOTIFY_ALL_SECONDARY_NODES_TIMER_INTERVAL) { if (_notifyAllSecondaryNodesTimerTriggered) return; _notifyAllSecondaryNodesTimer.Change(notifyInterval, Timeout.Infinite); _notifyAllSecondaryNodesTimerTriggered = true; } private async void NotifyAllSecondaryNodesTimerCallbackAsync(object state) { try { IReadOnlyDictionary clusterNodes = _clusterNodes; ClusterNode primaryNode = null; foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.Type == ClusterNodeType.Primary) { primaryNode = clusterNode.Value; break; } } if ((primaryNode is null) || (primaryNode.State != ClusterNodeState.Self)) throw new InvalidOperationException(); List tasks = new List(clusterNodes.Count); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.Type == ClusterNodeType.Primary) continue; //skip primary cluster node tasks.Add(clusterNode.Value.NotifySecondaryNodeAsync(primaryNode)); } await Task.WhenAll(tasks); //notify node call does error logging } catch (Exception ex) { _dnsWebService.LogManager.Write(ex); } finally { _notifyAllSecondaryNodesTimerTriggered = false; } } #endregion #region secondary node public async Task InitializeAndJoinClusterAsync(IReadOnlyList secondaryNodeIpAddresses, Uri primaryNodeUrl, string primaryNodeUsername, string primaryNodePassword, string primaryNodeTotp = null, IReadOnlyList primaryNodeIpAddresses = null, bool ignoreCertificateErrors = false, CancellationToken cancellationToken = default) { if (ClusterInitialized) throw new DnsServerException("Failed to join Cluster: the Cluster is already initialized."); if (!_dnsWebService.IsWebServiceTlsEnabled) throw new InvalidOperationException(); if (primaryNodeIpAddresses is null) { try { IReadOnlyList ipAddresses = await DnsClient.ResolveIPAsync(_dnsWebService.DnsServer, primaryNodeUrl.Host, _dnsWebService.DnsServer.PreferIPv6, cancellationToken); if (ipAddresses.Count < 1) throw new DnsServerException($"The domain name '{primaryNodeUrl.Host}' does not have an A/AAAA record configured."); primaryNodeIpAddresses = ipAddresses; } catch (Exception ex) { throw new DnsServerException($"Failed to join Cluster: the Primary node domain name '{primaryNodeUrl.Host}' could not be resolved to an IP address. Please specify the Primary node IP address manually.", ex); } } //login to primary node API using HttpApiClient primaryNodeApiClient = new HttpApiClient(primaryNodeUrl, _dnsWebService.DnsServer.Proxy, _dnsWebService.DnsServer.PreferIPv6, ignoreCertificateErrors, new InternalDnsClient(_dnsWebService.DnsServer, primaryNodeIpAddresses)); try { _ = await primaryNodeApiClient.LoginAsync(primaryNodeUsername, primaryNodePassword, primaryNodeTotp, false, cancellationToken); } catch (TwoFactorAuthRequiredHttpApiClientException ex) { throw new TwoFactorAuthRequiredWebServiceException("Failed to join Cluster: two-factor authentication is required by the Primary node user account.", ex); } try { //get cluster info ClusterInfo primaryNodeClusterInfo = await primaryNodeApiClient.GetClusterStateAsync(cancellationToken: cancellationToken); //do validations if (!primaryNodeClusterInfo.ClusterInitialized) throw new DnsServerException("Failed to join Cluster: the Primary node does not have a Cluster initialized."); string clusterCatalogDomain = "cluster-catalog." + primaryNodeClusterInfo.ClusterDomain; if (_dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain) is not null) throw new DnsServerException($"Failed to join Cluster: the zone '{clusterCatalogDomain}' already exists. Please delete the '{clusterCatalogDomain}' zone and try again."); if (_dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(primaryNodeClusterInfo.ClusterDomain) is not null) throw new DnsServerException($"Failed to join Cluster: the zone '{primaryNodeClusterInfo.ClusterDomain}' already exists. Please delete the '{primaryNodeClusterInfo.ClusterDomain}' zone and try again."); //create self node string serverDomain = _dnsWebService.DnsServer.ServerDomain; if (!serverDomain.EndsWith("." + primaryNodeClusterInfo.ClusterDomain, StringComparison.OrdinalIgnoreCase)) { int x = serverDomain.IndexOf('.'); if (x < 0) serverDomain = serverDomain + "." + primaryNodeClusterInfo.ClusterDomain; else serverDomain = string.Concat(serverDomain.AsSpan(0, x), ".", primaryNodeClusterInfo.ClusterDomain); } Uri secondaryNodeUrl = new Uri($"https://{serverDomain}:{_dnsWebService.WebServiceTlsPort}/"); ClusterNode selfSecondaryNode = new ClusterNode(this, RandomNumberGenerator.GetInt32(int.MaxValue), secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Self); //join cluster primaryNodeClusterInfo = await primaryNodeApiClient.JoinClusterAsync(selfSecondaryNode.Id, secondaryNodeUrl, secondaryNodeIpAddresses, _dnsWebService.WebServiceTlsCertificate, cancellationToken); //initialize cluster Dictionary clusterNodes = new Dictionary(primaryNodeClusterInfo.ClusterNodes.Count + 1); clusterNodes[selfSecondaryNode.Id] = selfSecondaryNode; foreach (ClusterInfo.ClusterNodeInfo nodeInfo in primaryNodeClusterInfo.ClusterNodes) { if (nodeInfo.Id == selfSecondaryNode.Id) continue; //skip self node ClusterNode node = new ClusterNode(this, nodeInfo); clusterNodes[node.Id] = node; } DisposeAllNodes(); //dispose existing nodes, if any _clusterNodes = clusterNodes; _clusterDomain = primaryNodeClusterInfo.ClusterDomain; _heartbeatRefreshIntervalSeconds = primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds; _heartbeatRetryIntervalSeconds = primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds; _configRefreshIntervalSeconds = primaryNodeClusterInfo.ConfigRefreshIntervalSeconds; _configRetryIntervalSeconds = primaryNodeClusterInfo.ConfigRetryIntervalSeconds; try { //sync entire config from primary node first to get TSIG keys for secondary catalog zone transfer _configLastSynced = DateTime.UnixEpoch; //reset last sync time to ensure full sync await SyncConfigFromAsync(primaryNodeApiClient, cancellationToken: cancellationToken); //create cluster secondary catalog zone AuthZoneInfo clusterSecondaryCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.CreateSecondaryCatalogZone(clusterCatalogDomain, primaryNodeIpAddresses.Convert(delegate (IPAddress ipAddress) { return new NameServerAddress(primaryNodeUrl.Host, ipAddress); }), DnsTransportProtocol.Tcp, clusterCatalogDomain); if (clusterSecondaryCatalogZoneInfo is null) throw new DnsServerException($"Failed to join Cluster: the zone '{clusterCatalogDomain}' already exists. Please delete the '{clusterCatalogDomain}' zone and try again."); //set cluster secondary catalog zone permissions _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterSecondaryCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService.AuthManager.SetPermission(PermissionSection.Zones, clusterSecondaryCatalogZoneInfo.Name, _dnsWebService.AuthManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.View); } catch { try { await primaryNodeApiClient.DeleteSecondaryNodeAsync(selfSecondaryNode.Id, cancellationToken); } catch { } DeleteAllClusterConfig(); throw; } //initialize heartbeat timer for all nodes here since config sync and zone transfers needs to occur first for DANE validation to work InitializeHeartbeatTimerFor(clusterNodes); //start config refresh timer to refresh as per config refresh interval as config was just synced UpdateConfigRefreshTimer(_configRefreshIntervalSeconds * 1000); //finalize _dnsWebService.DnsServer.ServerDomain = selfSecondaryNode.Name; //save all changes _dnsWebService.DnsServer.SaveConfigFile(); _dnsWebService.AuthManager.SaveConfigFile(); SaveConfigFile(); } finally { try { //logout from primary node await primaryNodeApiClient.LogoutAsync(cancellationToken); } catch { } } } public async Task LeaveClusterAsync(bool forceLeave) { if (!ClusterInitialized) throw new DnsServerException("Failed to leave Cluster: the Cluster is not initialized."); ClusterNode primaryNode = GetPrimaryNode(); if (primaryNode.State == ClusterNodeState.Self) throw new DnsServerException("Failed to leave Cluster: a Primary self node cannot leave the Cluster."); ClusterNode secondaryNode = GetSelfNode(); if (secondaryNode.Type != ClusterNodeType.Secondary) throw new DnsServerException("Failed to leave Cluster: only Secondary nodes can leave the Cluster."); if (!forceLeave) { //delete self node from cluster on primary node await primaryNode.DeleteSecondaryNodeAsync(secondaryNode); } //delete all cluster config DeleteAllClusterConfig(); } public async Task UpdatePrimaryNodeAsync(Uri primaryNodeUrl, IReadOnlyList primaryNodeIpAddresses = null, int primaryNodeId = -1, CancellationToken cancellationToken = default) { if (!ClusterInitialized) throw new DnsServerException("Failed to update Primary node: the Cluster is not initialized."); if (primaryNodeIpAddresses is null) { try { IReadOnlyList ipAddresses = await DnsClient.ResolveIPAsync(_dnsWebService.DnsServer, primaryNodeUrl.Host, _dnsWebService.DnsServer.PreferIPv6, cancellationToken); if (ipAddresses.Count < 1) throw new DnsServerException($"The domain name '{primaryNodeUrl.Host}' does not have an A/AAAA record configured."); primaryNodeIpAddresses = ipAddresses; } catch (Exception ex) { throw new DnsServerException($"Failed to update Primary node: the Primary node domain name '{primaryNodeUrl.Host}' could not be resolved to an IP address.", ex); } } ClusterNode primaryNode; if (primaryNodeId < 0) primaryNode = GetPrimaryNode(); else if (!_clusterNodes.TryGetValue(primaryNodeId, out primaryNode)) throw new DnsServerException("Failed to update Primary node: the specified Primary node ID does not exists in the Cluster."); if (primaryNode.State == ClusterNodeState.Self) throw new DnsServerException("Failed to update Primary node: the specified node is the self node and cannot be updated this way."); if (primaryNode.Type == ClusterNodeType.Secondary) { //secondary node was promoted to primary node ClusterNode formerPrimaryNode = GetPrimaryNode(); //dispose former primary node immediately to stop heartbeat formerPrimaryNode.Dispose(); //remove former primary node from cluster nodes IReadOnlyDictionary existingClusterNodes = _clusterNodes; Dictionary updatedClusterNodes = new Dictionary(existingClusterNodes.Count - 1); foreach (KeyValuePair existingClusterNode in existingClusterNodes) { if (existingClusterNode.Key == formerPrimaryNode.Id) continue; updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value; } //update cluster nodes _clusterNodes = updatedClusterNodes; //promote secondary node to primary immediately primaryNode.PromoteToPrimaryNode(); //ensure to save changes SaveConfigFile(); } //validate for duplicate names foreach (KeyValuePair clusterNode in _clusterNodes) { if (clusterNode.Key == primaryNode.Id) continue; //skip self if (clusterNode.Value.Name.Equals(primaryNodeUrl.Host, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("Failed to update Primary node: the Primary node's domain name already exists in the Cluster. Please try again after changing the Primary DNS Server's domain name."); } //get cluster secondary catalog zone string clusterCatalogDomain = "cluster-catalog." + _clusterDomain; AuthZoneInfo clusterSecondaryCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain); if (clusterSecondaryCatalogZoneInfo is null) throw new DnsServerException($"Failed to update Primary node: the Cluster Secondary Catalog zone '{clusterCatalogDomain}' does not exists."); //update primary node primaryNode.UpdateNode(primaryNodeUrl, primaryNodeIpAddresses); //update cluster catalog zone's primary name server clusterSecondaryCatalogZoneInfo.PrimaryNameServerAddresses = primaryNodeIpAddresses.Convert(delegate (IPAddress ipAddress) { return new NameServerAddress(primaryNodeUrl.Host, ipAddress); }); //save all changes _dnsWebService.DnsServer.AuthZoneManager.SaveZoneFile(clusterSecondaryCatalogZoneInfo.Name); SaveConfigFile(); //trigger config and zone refresh TriggerRefreshForConfig(CONFIG_REFRESH_TIMER_INTERVAL); return primaryNode; } public void TriggerRefreshForConfig(IReadOnlyCollection configRefreshIncludeZones = null) { //do validation if (!ClusterInitialized) throw new DnsServerException("Failed to refresh configuration: the Cluster is not initialized."); ClusterNode primaryNode = GetPrimaryNode(); if (primaryNode.State == ClusterNodeState.Self) throw new DnsServerException("Failed to refresh configuration: only Secondary nodes can sync configuration from Primary nodes."); TriggerRefreshForConfig(CONFIG_REFRESH_TIMER_INTERVAL, configRefreshIncludeZones); } public void TriggerResyncForConfig() { //do validation if (!ClusterInitialized) throw new DnsServerException("Failed to resync configuration: the Cluster is not initialized."); ClusterNode primaryNode = GetPrimaryNode(); if (primaryNode.State == ClusterNodeState.Self) throw new DnsServerException("Failed to resync configuration: only Secondary nodes can sync configuration from Primary nodes."); _configLastSynced = DateTime.UnixEpoch; //to ensure complete config resync //trigger immediate config refresh TriggerRefreshForConfig(0); } private void TriggerRefreshForConfig(int refreshInterval, IReadOnlyCollection configRefreshIncludeZones = null) { _configRefreshLock.Wait(); try { if (configRefreshIncludeZones is not null) { if (_configRefreshIncludeZones is null) _configRefreshIncludeZones = configRefreshIncludeZones; else _configRefreshIncludeZones = [.. _configRefreshIncludeZones, .. configRefreshIncludeZones]; } if (_configRefreshTimerTriggered) return; _configRefreshTimer.Change(refreshInterval, Timeout.Infinite); _configRefreshTimerTriggered = true; } finally { _configRefreshLock.Release(); } } private async void ConfigRefreshTimerCallbackAsync(object state) { bool success = false; await _configRefreshLock.WaitAsync(); try { ClusterNode primaryNode = GetPrimaryNode(); if (primaryNode.State == ClusterNodeState.Self) throw new InvalidOperationException(); //update cluster options UpdateClusterFromPrimaryNode(await primaryNode.GetClusterStateAsync()); //sync config from primary node await primaryNode.SyncConfigAsync(_configRefreshIncludeZones); success = true; } catch (Exception ex) { _dnsWebService.LogManager.Write("Failed to sync server configuration from the Primary node.\r\n" + ex.ToString()); } finally { if (success) { _configRefreshTimerTriggered = false; _configRefreshIncludeZones = null; } try { _configRefreshTimer.Change(success ? _configRefreshIntervalSeconds * 1000 : _configRetryIntervalSeconds * 1000, Timeout.Infinite); } catch (ObjectDisposedException) { } try { _configRefreshLock.Release(); } catch (ObjectDisposedException) { } } } public async Task SyncConfigFromAsync(HttpApiClient primaryNodeApiClient, IReadOnlyCollection includeZones = null, CancellationToken cancellationToken = default) { string tmpFile = Path.GetTempFileName(); try { await using (FileStream configZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //get config from primary node (Stream, DateTime) response = await primaryNodeApiClient.TransferConfigFromPrimaryNodeAsync(_configLastSynced, includeZones, cancellationToken); await using (Stream stream = response.Item1) { await stream.CopyToAsync(configZipStream, cancellationToken); } //dynamically load config configZipStream.Position = 0; await _dnsWebService.RestoreConfigAsync(zipStream: configZipStream, authConfig: true, clusterConfig: false, webServiceSettings: false, dnsSettings: true, logSettings: false, zones: true, allowedZones: true, blockedZones: true, blockLists: true, apps: true, scopes: false, stats: false, logs: false, deleteExistingFiles: false, isConfigTransfer: true); _configLastSynced = response.Item2; //save config SaveConfigFile(); } _dnsWebService.LogManager.Write("Server configuration was synced from the Primary node successfully."); } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService.LogManager.Write(ex); } } } private void TriggerClusterUpdateForSecondaryNodeChanges() { if (_clusterUpdateForSecondaryNodeChangesTimerTriggered) return; _clusterUpdateForSecondaryNodeChangesTimer.Change(CLUSTER_UPDATE_FOR_SECONDARY_NODE_CHANGES_TIMER_INTERVAL, Timeout.Infinite); _clusterUpdateForSecondaryNodeChangesTimerTriggered = true; } private async void ClusterUpdateForSecondaryNodeChangesTimerCallbackAsync(object state) { try { ClusterNode primaryNode = GetPrimaryNode(); if (primaryNode.State == ClusterNodeState.Self) throw new InvalidOperationException(); ClusterNode secondaryNode = GetSelfNode(); if (secondaryNode.Type != ClusterNodeType.Secondary) throw new InvalidOperationException(); UpdateClusterFromPrimaryNode(await primaryNode.UpdateSecondaryNodeAsync(secondaryNode, _dnsWebService.WebServiceTlsCertificate)); _dnsWebService.LogManager.Write("DNS Server updated this Secondary node's details on the Primary node successfully."); } catch (Exception ex) { _dnsWebService.LogManager.Write("DNS Server failed to update this Secondary node's details on the Primary node." + ex.ToString()); } finally { _clusterUpdateForSecondaryNodeChangesTimerTriggered = false; } } public void UpdateClusterFromPrimaryNode(ClusterInfo primaryNodeClusterInfo) { IReadOnlyDictionary existingClusterNodes = _clusterNodes; //validation foreach (KeyValuePair existingClusterNode in existingClusterNodes) { if (existingClusterNode.Value.Type == ClusterNodeType.Primary) { if (existingClusterNode.Value.State == ClusterNodeState.Self) throw new InvalidOperationException(); //this is a self primary node itself break; } } List clusterNodesToAdd = new List(); List clusterNodesToRemove = new List(); foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in primaryNodeClusterInfo.ClusterNodes) { if (existingClusterNodes.TryGetValue(clusterNodeInfo.Id, out ClusterNode existingClusterNode)) { if (existingClusterNode.State == ClusterNodeState.Self) continue; //skip self node //update existing cluster node existingClusterNode.UpdateNode(clusterNodeInfo); } else { //add new cluster node clusterNodesToAdd.Add(new ClusterNode(this, clusterNodeInfo)); } } foreach (KeyValuePair existingClusterNode in existingClusterNodes) { bool found = false; foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in primaryNodeClusterInfo.ClusterNodes) { if (existingClusterNode.Key == clusterNodeInfo.Id) { found = true; break; } } if (!found) clusterNodesToRemove.Add(existingClusterNode.Value); } bool saveConfig = false; if ((clusterNodesToAdd.Count > 0) || (clusterNodesToRemove.Count > 0)) { Dictionary updatedClusterNodes = new Dictionary(existingClusterNodes.Count + clusterNodesToAdd.Count - clusterNodesToRemove.Count); foreach (KeyValuePair existingClusterNode in existingClusterNodes) { if (clusterNodesToRemove.Contains(existingClusterNode.Value)) continue; //skip removed node updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value; } foreach (ClusterNode clusterNode in clusterNodesToAdd) updatedClusterNodes[clusterNode.Id] = clusterNode; //verify if node is part of the cluster { bool foundSelfNode = false; foreach (KeyValuePair clusterNode in updatedClusterNodes) { if (clusterNode.Value.State == ClusterNodeState.Self) { foundSelfNode = true; break; } } if (!foundSelfNode) { //this node is not part of the cluster anymore //delete all cluster config DeleteAllClusterConfig(); _dnsWebService.LogManager.Write("Failed to sync Cluster config: this Secondary node is not part of the Cluster anymore."); return; } } //dispose all removed nodes foreach (ClusterNode removedNodes in clusterNodesToRemove) removedNodes.Dispose(); _clusterNodes = updatedClusterNodes; InitializeHeartbeatTimerFor(updatedClusterNodes); saveConfig = true; } if (primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds != _heartbeatRefreshIntervalSeconds) { _heartbeatRefreshIntervalSeconds = primaryNodeClusterInfo.HeartbeatRefreshIntervalSeconds; UpdateHeartbeatTimerForAllClusterNodes(); //apply new interval to all cluster nodes immediately saveConfig = true; } if (primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds != _heartbeatRetryIntervalSeconds) { _heartbeatRetryIntervalSeconds = primaryNodeClusterInfo.HeartbeatRetryIntervalSeconds; saveConfig = true; } if (primaryNodeClusterInfo.ConfigRefreshIntervalSeconds != _configRefreshIntervalSeconds) { _configRefreshIntervalSeconds = primaryNodeClusterInfo.ConfigRefreshIntervalSeconds; UpdateConfigRefreshTimer(_configRefreshIntervalSeconds * 1000); //apply new interval to config refresh timer immediately saveConfig = true; } if (primaryNodeClusterInfo.ConfigRetryIntervalSeconds != _configRetryIntervalSeconds) { _configRetryIntervalSeconds = primaryNodeClusterInfo.ConfigRetryIntervalSeconds; saveConfig = true; } //save changes if (saveConfig) SaveConfigFile(); } public async Task PromoteToPrimaryNodeAsync(bool forceDeletePrimary) { if (!ClusterInitialized) throw new DnsServerException("Failed to promote to Primary node: the Cluster is not initialized."); //do validation ClusterNode selfNewPrimaryNode = GetSelfNode(); if (selfNewPrimaryNode.Type != ClusterNodeType.Secondary) throw new DnsServerException("Failed to promote to Primary node: only Secondary nodes can be promoted to Primary nodes."); string clusterCatalogDomain = "cluster-catalog." + _clusterDomain; AuthZoneInfo clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(clusterCatalogDomain); if (clusterCatalogZoneInfo is null) throw new DnsServerException("Failed to promote to Primary node: the Cluster Secondary Catalog zone does not exist."); AuthZoneInfo clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(_clusterDomain); if (clusterZoneInfo is null) throw new DnsServerException("Failed to promote to Primary node: the Cluster Secondary zone does not exist."); //stop cluster config refresh timer StopConfigRefreshTimer(); //resync config and delete current primary node from the cluster immediately ClusterNode existingPrimaryNode = GetPrimaryNode(); if (!forceDeletePrimary) { //resync complete config from current primary node to ensure all data is synced _configLastSynced = DateTime.UnixEpoch; //to ensure complete config resync await existingPrimaryNode.SyncConfigAsync(); //delete current cluster primary node await existingPrimaryNode.DeleteClusterAsync(true); } //dispose primary node immediately to stop heartbeat existingPrimaryNode.Dispose(); //remove primary node from cluster nodes IReadOnlyDictionary existingClusterNodes = _clusterNodes; Dictionary updatedClusterNodes = new Dictionary(existingClusterNodes.Count - 1); foreach (KeyValuePair existingClusterNode in existingClusterNodes) { if (existingClusterNode.Key == existingPrimaryNode.Id) continue; updatedClusterNodes[existingClusterNode.Key] = existingClusterNode.Value; } //update cluster nodes _clusterNodes = updatedClusterNodes; //promote self node to primary immediately selfNewPrimaryNode.PromoteToPrimaryNode(); //convert cluster secondary catalog zone to catalog zone along with all its member zones if (clusterCatalogZoneInfo.Type == AuthZoneType.SecondaryCatalog) clusterCatalogZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.ConvertZoneTypeTo(clusterCatalogZoneInfo.Name, AuthZoneType.Catalog); //get converted primary cluster zone info clusterZoneInfo = _dnsWebService.DnsServer.AuthZoneManager.GetAuthZoneInfo(_clusterDomain); if (clusterZoneInfo is null) throw new DnsServerException("Failed to promote to Primary node: the Cluster Primary zone does not exist."); //sign cluster zone in case when DNSSEC private keys were not available during ConvertZoneTypeTo() operation if (clusterZoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned) { DnssecPrivateKey kskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.KeySigningKey); DnssecPrivateKey zskPrivateKey = DnssecPrivateKey.Create(DnssecAlgorithm.ECDSAP256SHA256, DnssecPrivateKeyType.ZoneSigningKey); zskPrivateKey.RolloverDays = 90; _dnsWebService.DnsServer.AuthZoneManager.SignPrimaryZone(clusterZoneInfo.Name, kskPrivateKey, zskPrivateKey, 3600, false); } //find existing record TTL values FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //remove old primary node records from cluster primary zone and save zone file if (existingPrimaryNode is not null) RemoveClusterPrimaryZoneRecordsFor(existingPrimaryNode); //update cluster primary zone for new primary node AddClusterPrimaryZoneRecordsFor(selfNewPrimaryNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate); //update cluster catalog zone ACLs, TSIG key name and save zone file UpdateClusterCatalogZoneOptions(clusterCatalogZoneInfo); //save all changes SaveConfigFile(); //notify all secondary nodes as a primary node immediately TriggerNotifyAllSecondaryNodes(0); //trigger NS and SOA update for member zones TriggerRecordUpdateForClusterCatalogMemberZones(); } #endregion #region public public ClusterNode GetPrimaryNode() { IReadOnlyDictionary clusterNodes = _clusterNodes; if (clusterNodes is null) throw new InvalidOperationException(); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.Type == ClusterNodeType.Primary) return clusterNode.Value; } throw new InvalidOperationException(); } public ClusterNode GetSelfNode() { IReadOnlyDictionary clusterNodes = _clusterNodes; if (clusterNodes is null) throw new InvalidOperationException(); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.State == ClusterNodeState.Self) return clusterNode.Value; } throw new InvalidOperationException(); } public bool TryGetClusterNode(string nodeName, out ClusterNode clusterNode) { foreach (KeyValuePair node in _clusterNodes) { if (node.Value.Name.Equals(nodeName, StringComparison.OrdinalIgnoreCase)) { clusterNode = node.Value; return true; } } clusterNode = null; return false; } public bool IsClusterPrimaryZone(string zoneName) { return (zoneName is not null) && zoneName.Equals(_clusterDomain, StringComparison.OrdinalIgnoreCase); } public bool IsClusterCatalogZone(string zoneName) { return (zoneName is not null) && zoneName.Equals("cluster-catalog." + _clusterDomain, StringComparison.OrdinalIgnoreCase); } public ClusterNode UpdateSelfNodeIPAddresses(IReadOnlyList ipAddresses) { ClusterNode selfNode = GetSelfNode(); switch (selfNode.Type) { case ClusterNodeType.Primary: //find existing record TTL values FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //update cluster zone to remove current self node records RemoveClusterPrimaryZoneRecordsFor(selfNode); //update self node selfNode.UpdateSelfNodeIPAddresses(ipAddresses); //update cluster zone to add updated self node records AddClusterPrimaryZoneRecordsFor(selfNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate); //update cluster catalog zone ACLs and save zone file UpdateClusterCatalogZoneOptions(); //save all changes SaveConfigFile(); //notify all secondary nodes immediately TriggerNotifyAllSecondaryNodes(0); break; case ClusterNodeType.Secondary: //update self node selfNode.UpdateSelfNodeIPAddresses(ipAddresses); //save all changes SaveConfigFile(); //trigger cluster node update on primary node TriggerClusterUpdateForSecondaryNodeChanges(); break; } return selfNode; } public void UpdateSelfNodeUrlAndCertificate() { ClusterNode selfNode = GetSelfNode(); //validation foreach (KeyValuePair clusterNode in _clusterNodes) { if (clusterNode.Key == selfNode.Id) continue; //skip self if (clusterNode.Value.Name.Equals(_dnsWebService.DnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("Failed to update self node URL: the node's domain name already exists in the Cluster. Please try again after changing the DNS Server's domain name."); } switch (selfNode.Type) { case ClusterNodeType.Primary: //find existing record TTL values FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //update cluster zone to remove current self node records RemoveClusterPrimaryZoneRecordsFor(selfNode); //update self node selfNode.UpdateSelfNodeUrl(); //update cluster zone to add updated self node records AddClusterPrimaryZoneRecordsFor(selfNode, nsTtl, aTtl, _dnsWebService.WebServiceTlsCertificate); //save all changes SaveConfigFile(); _dnsWebService.LogManager.Write("Primary node '" + selfNode.ToString() + "' URL was updated successfully."); //notify all secondary nodes TriggerNotifyAllSecondaryNodes(); break; case ClusterNodeType.Secondary: //update self node selfNode.UpdateSelfNodeUrl(); //save all changes SaveConfigFile(); _dnsWebService.LogManager.Write("Secondary node '" + selfNode.ToString() + "' URL was updated successfully."); //trigger cluster node update on primary node TriggerClusterUpdateForSecondaryNodeChanges(); break; } } #endregion #region properties public DnsWebService DnsWebService { get { return _dnsWebService; } } public bool ClusterInitialized { get { IReadOnlyDictionary clusterNodes = _clusterNodes; return (clusterNodes is not null) && (clusterNodes.Count > 0); } } public string ClusterDomain { get { return _clusterDomain; } } public ushort HeartbeatRefreshIntervalSeconds { get { return _heartbeatRefreshIntervalSeconds; } } public ushort HeartBeatRetryIntervalSeconds { get { return _heartbeatRetryIntervalSeconds; } } public ushort ConfigRefreshIntervalSeconds { get { return _configRefreshIntervalSeconds; } } public ushort ConfigRetryIntervalSeconds { get { return _configRetryIntervalSeconds; } } public DateTime ConfigLastSynced { get { return _configLastSynced; } } public IReadOnlyDictionary ClusterNodes { get { return _clusterNodes; } } #endregion } } ================================================ FILE: DnsServerCore/Cluster/ClusterNode.cs ================================================ /* 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.HttpApi; using DnsServerCore.HttpApi.Models; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; namespace DnsServerCore.Cluster { enum ClusterNodeType : byte { Unknown = 0, Primary = 1, Secondary = 2 } enum ClusterNodeState : byte { Unknown = 0, Self = 1, Connected = 2, Unreachable = 3 } class ClusterNode : IComparable, IDisposable { #region variables readonly ClusterManager _clusterManager; readonly int _id; Uri _url; IReadOnlyList _ipAddresses; ClusterNodeType _type; ClusterNodeState _state; DateTime _upSince; DateTime _lastSeen; HttpApiClient _apiClient; Timer _heartbeatTimer; const int HEARTBEAT_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor public ClusterNode(ClusterManager clusterManager, ClusterInfo.ClusterNodeInfo nodeInfo) { _clusterManager = clusterManager; _id = nodeInfo.Id; _url = nodeInfo.Url; _ipAddresses = nodeInfo.IPAddresses.Convert(IPAddress.Parse); _type = Enum.Parse(nodeInfo.Type, true); if (_type == ClusterNodeType.Primary) { _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; //since this info was received from primary node } else { _state = ClusterNodeState.Unknown; } } public ClusterNode(ClusterManager clusterManager, int id, Uri url, IReadOnlyList ipAddresses, ClusterNodeType type, ClusterNodeState state) { if (url.OriginalString.Length > 255) throw new ArgumentException("Cluster node URL length must be less than 255 bytes.", nameof(url)); if (!url.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Cluster node URL must use HTTPS scheme.", nameof(url)); if (ipAddresses.Count > 10) throw new ArgumentException("Cluster node cannot have more than 10 IP addresses.", nameof(ipAddresses)); _clusterManager = clusterManager; _id = id; _url = url; _ipAddresses = ipAddresses; _type = type; _state = state; } public ClusterNode(ClusterManager clusterManager, BinaryReader bR) { _clusterManager = clusterManager; int version = bR.ReadByte(); switch (version) { case 1: case 2: _id = bR.ReadInt32(); _url = new Uri(bR.ReadShortString()); if (version >= 2) { int count = bR.ReadByte(); IPAddress[] ipAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) ipAddresses[i] = IPAddressExtensions.ReadFrom(bR); _ipAddresses = ipAddresses; } else { _ipAddresses = [IPAddressExtensions.ReadFrom(bR)]; } _type = (ClusterNodeType)bR.ReadByte(); _state = (ClusterNodeState)bR.ReadByte(); break; default: throw new InvalidDataException("Cluster Node version not supported."); } if (_state != ClusterNodeState.Self) _state = ClusterNodeState.Unknown; } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _heartbeatTimer?.Dispose(); if (_apiClient is not null) { ThreadPool.QueueUserWorkItem(async delegate (object state) { try { await Task.Delay(2000); //give some time for any in-progress API calls to complete _apiClient?.Dispose(); } catch { } }); } _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private HttpApiClient GetApiClient() { if (_state == ClusterNodeState.Self) throw new InvalidOperationException(); if (_apiClient is null) { _apiClient = new HttpApiClient(_url, _clusterManager.DnsWebService.DnsServer.Proxy, _clusterManager.DnsWebService.DnsServer.PreferIPv6, false, new InternalDnsClient(_clusterManager.DnsWebService.DnsServer, this)); UserSession clusterApiToken = null; foreach (UserSession session in _clusterManager.DnsWebService.AuthManager.Sessions) { if ((session.Type == UserSessionType.ApiToken) && (session.TokenName == _clusterManager.ClusterDomain)) { clusterApiToken = session; break; } } if (clusterApiToken is null) throw new InvalidOperationException("No API token was found for the Cluster domain."); _apiClient.UseApiToken(clusterApiToken.Token); } return _apiClient; } private async void HeartbeatTimerCallbackAsync(object state) { bool success = true; try { ClusterInfo clusterInfo = await GetClusterStateAsync(); if (_type == ClusterNodeType.Primary) _clusterManager.UpdateClusterFromPrimaryNode(clusterInfo); //update cluster nodes from primary node response //update up since time foreach (ClusterInfo.ClusterNodeInfo clusterNodeInfo in clusterInfo.ClusterNodes) { if (clusterNodeInfo.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)) { _upSince = clusterNodeInfo.UpSince ?? default; break; } } } catch (TaskCanceledException) { //ignore } catch (Exception ex) { success = false; _clusterManager.DnsWebService.LogManager.Write("Heartbeat failed for " + _type.ToString() + " node '" + ToString() + "'.\r\n" + ex.ToString()); } finally { try { _heartbeatTimer?.Change(success ? _clusterManager.HeartbeatRefreshIntervalSeconds * 1000 : _clusterManager.HeartBeatRetryIntervalSeconds * 1000, Timeout.Infinite); } catch (ObjectDisposedException) { } } } #endregion #region public public void PromoteToPrimaryNode() { _type = ClusterNodeType.Primary; } public void UpdateSelfNodeIPAddresses(IReadOnlyList ipAddresses) { if (_state != ClusterNodeState.Self) throw new InvalidOperationException(); if (ipAddresses.Count > 10) throw new ArgumentException("Cluster node cannot have more than 10 IP addresses.", nameof(ipAddresses)); _ipAddresses = ipAddresses; } public void UpdateSelfNodeUrl() { if (_state != ClusterNodeState.Self) throw new InvalidOperationException(); Uri url = new Uri($"https://{_clusterManager.DnsWebService.DnsServer.ServerDomain}:{_clusterManager.DnsWebService.WebServiceTlsPort}/"); if (url.OriginalString.Length > 255) throw new ArgumentException("Cluster node URL length must be less than 255 bytes.", nameof(url)); if (!url.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Cluster node URL must use HTTPS scheme.", nameof(url)); _url = url; } public void UpdateNode(Uri url, IReadOnlyList ipAddresses) { if (url.OriginalString.Length > 255) throw new ArgumentException("Cluster node URL length must be less than 255 bytes.", nameof(url)); if (!url.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Cluster node URL must use HTTPS scheme.", nameof(url)); if (ipAddresses.Count > 10) throw new ArgumentException("Cluster node cannot have more than 10 IP addresses.", nameof(ipAddresses)); bool changed = false; if (!_url.Equals(url)) { _url = url; changed = true; } if (!_ipAddresses.HasSameItems(ipAddresses)) { _ipAddresses = ipAddresses; changed = true; } if (changed && (_apiClient is not null)) { _apiClient.Dispose(); _apiClient = null; } } public void UpdateNode(ClusterInfo.ClusterNodeInfo nodeInfo) { if (nodeInfo.Id != _id) throw new InvalidOperationException(); bool changed = false; if (!_url.Equals(nodeInfo.Url)) { _url = nodeInfo.Url; changed = true; } IReadOnlyList ipAddresses = nodeInfo.IPAddresses.Convert(IPAddress.Parse); if (!_ipAddresses.HasSameItems(ipAddresses)) { _ipAddresses = ipAddresses; changed = true; } _type = Enum.Parse(nodeInfo.Type, true); if (changed && (_apiClient is not null)) { _apiClient.Dispose(); _apiClient = null; } } public void InitializeHeartbeatTimer() { if (_state == ClusterNodeState.Self) throw new InvalidOperationException(); if (_heartbeatTimer is null) { _heartbeatTimer = new Timer(HeartbeatTimerCallbackAsync); //for Primary node use configured refresh interval since config transfer already syncs the cluster state _heartbeatTimer.Change(_type == ClusterNodeType.Primary ? _clusterManager.HeartbeatRefreshIntervalSeconds * 1000 : HEARTBEAT_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } public void UpdateHeartbeatTimer() { if (_state == ClusterNodeState.Self) throw new InvalidOperationException(); _heartbeatTimer?.Change(_clusterManager.HeartbeatRefreshIntervalSeconds * 1000, Timeout.Infinite); } public async Task GetDashboardStatsAsync(User sessionUser, DashboardStatsType type = DashboardStatsType.LastHour, bool utcFormat = false, string acceptLanguage = "en-US,en;q=0.5", bool dontTrimQueryTypeData = false, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default) { HttpApiClient apiClient = GetApiClient(); try { DashboardStats stats = await apiClient.GetDashboardStatsAsync(sessionUser.Username, type, utcFormat, acceptLanguage, dontTrimQueryTypeData, startDate, endDate, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; return stats; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task GetDashboardTopStatsAsync(User sessionUser, DashboardTopStatsType statsType, int limit = 1000, DashboardStatsType type = DashboardStatsType.LastHour, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default) { HttpApiClient apiClient = GetApiClient(); try { DashboardStats stats = await apiClient.GetDashboardTopStatsAsync(sessionUser.Username, statsType, limit, type, startDate, endDate, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; return stats; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task SetClusterSettingsAsync(User sessionUser, IReadOnlyDictionary clusterParameters, CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Primary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { await apiClient.SetClusterSettingsAsync(sessionUser.Username, clusterParameters, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task ForceUpdateBlockListsAsync(User sessionUser, CancellationToken cancellationToken = default) { HttpApiClient apiClient = GetApiClient(); try { await apiClient.ForceUpdateBlockListsAsync(sessionUser.Username, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task TemporaryDisableBlockingAsync(User sessionUser, int minutes, CancellationToken cancellationToken = default) { HttpApiClient apiClient = GetApiClient(); try { await apiClient.TemporaryDisableBlockingAsync(sessionUser.Username, minutes, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task GetClusterStateAsync(CancellationToken cancellationToken = default) { HttpApiClient apiClient = GetApiClient(); try { ClusterInfo clusterInfo = await apiClient.GetClusterStateAsync(false, true, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; return clusterInfo; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task DeleteClusterAsync(bool forceDelete = false, CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Primary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { ClusterInfo clusterInfo = await apiClient.DeleteClusterAsync(forceDelete, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Unreachable; //node is deleted, so mark as unreachable return clusterInfo; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task NotifySecondaryNodeAsync(ClusterNode primaryNode, CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Secondary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { await apiClient.NotifySecondaryNodeAsync(primaryNode.Id, primaryNode._url, primaryNode._ipAddresses, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; _clusterManager.DnsWebService.LogManager.Write("DNS Server successfully notified Secondary node '" + ToString() + "' for server configuration changes."); } catch (Exception ex) { _state = ClusterNodeState.Unreachable; _clusterManager.DnsWebService.LogManager.Write("DNS Server failed to notify Secondary node '" + ToString() + "' for server configuration changes.\r\n" + ex.ToString()); } } public async Task SyncConfigAsync(IReadOnlyCollection includeZones = null, CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Primary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { await _clusterManager.SyncConfigFromAsync(apiClient, includeZones, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task AskSecondaryNodeToLeaveClusterAsync(CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Secondary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { _ = await apiClient.LeaveClusterAsync(cancellationToken: cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task DeleteSecondaryNodeAsync(ClusterNode secondaryNode, CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Primary) throw new InvalidOperationException(); if (secondaryNode.Type != ClusterNodeType.Secondary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { await apiClient.DeleteSecondaryNodeAsync(secondaryNode._id, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task UpdateSecondaryNodeAsync(ClusterNode secondaryNode, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default) { if (_type != ClusterNodeType.Primary) throw new InvalidOperationException(); if (secondaryNode.Type != ClusterNodeType.Secondary) throw new InvalidOperationException(); HttpApiClient apiClient = GetApiClient(); try { ClusterInfo clusterInfo = await apiClient.UpdateSecondaryNodeAsync(secondaryNode._id, secondaryNode._url, secondaryNode._ipAddresses, secondaryNodeCertificate, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; return clusterInfo; } catch { _state = ClusterNodeState.Unreachable; throw; } } public async Task ProxyRequest(HttpContext context, string actingUsername, CancellationToken cancellationToken = default) { HttpApiClient apiClient = GetApiClient(); try { await apiClient.ProxyRequest(context, actingUsername, cancellationToken); _lastSeen = DateTime.UtcNow; _state = ClusterNodeState.Connected; } catch { _state = ClusterNodeState.Unreachable; throw; } } public void WriteTo(BinaryWriter bW) { bW.Write((byte)2); //version bW.Write(_id); bW.WriteShortString(_url.OriginalString); bW.Write(Convert.ToByte(_ipAddresses.Count)); foreach (IPAddress ipAddress in _ipAddresses) ipAddress.WriteTo(bW); bW.Write((byte)_type); bW.Write((byte)_state); } public override string ToString() { return _url.Host.ToLowerInvariant() + " (" + _ipAddresses.Join() + ")"; } public int CompareTo(ClusterNode other) { return _url.Host.CompareTo(other._url.Host); } #endregion #region properties public int Id { get { return _id; } } public string Name { get { return _url.Host.ToLowerInvariant(); } } public Uri Url { get { return _url; } } public IReadOnlyList IPAddresses { get { return _ipAddresses; } } public ClusterNodeType Type { get { return _type; } } public ClusterNodeState State { get { return _state; } } public DateTime UpSince { get { if (_state == ClusterNodeState.Self) return _clusterManager.DnsWebService.UpTimeStamp; return _upSince; } } public DateTime LastSeen { get { return _lastSeen; } } #endregion } } ================================================ FILE: DnsServerCore/Cluster/InternalDnsClient.cs ================================================ /* 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.Dns; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Cluster { class InternalDnsClient : IDnsClient { #region variables readonly DnsServer _dnsServer; readonly ClusterNode _clusterNode; readonly IReadOnlyList _ipAddresses; #endregion #region constructor public InternalDnsClient(DnsServer dnsServer, ClusterNode clusterNode) { _dnsServer = dnsServer; _clusterNode = clusterNode; } public InternalDnsClient(DnsServer dnsServer, IReadOnlyList ipAddresses) { _dnsServer = dnsServer; _ipAddresses = ipAddresses; } #endregion #region protected public Task ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default) { switch (question.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: IReadOnlyList ipAddresses; if (_clusterNode is null) ipAddresses = _ipAddresses; else ipAddresses = _clusterNode.IPAddresses; List answer = new List(); foreach (IPAddress ipAddress in ipAddresses) { DnsResourceRecordData rdata = null; switch (ipAddress.AddressFamily) { case AddressFamily.InterNetwork: if (question.Type == DnsResourceRecordType.A) rdata = new DnsARecordData(ipAddress); break; case AddressFamily.InterNetworkV6: if (question.Type == DnsResourceRecordType.AAAA) rdata = new DnsAAAARecordData(ipAddress); break; } if (rdata is not null) answer.Add(new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, 30, rdata)); } return Task.FromResult(new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, DnsResponseCode.NoError, [question], answer)); default: DirectDnsClient dnsClient = new DirectDnsClient(_dnsServer); dnsClient.DnssecValidation = true; //load latest trust anchors into dns client _dnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, question.Name, question.Type); return dnsClient.ResolveAsync(question, cancellationToken); } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/DhcpMessage.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.Dhcp.Options; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp { enum DhcpMessageOpCode : byte { BootRequest = 1, BootReply = 2 } enum DhcpMessageHardwareAddressType : byte { Ethernet = 1 } enum DhcpMessageFlags : ushort { None = 0, Broadcast = 0x8000 } class DhcpMessage { #region variables const uint MAGIC_COOKIE = 0x63538263; //in reverse format readonly DhcpMessageOpCode _op; readonly DhcpMessageHardwareAddressType _htype; readonly byte _hlen; readonly byte _hops; readonly byte[] _xid; readonly byte[] _secs; readonly DhcpMessageFlags _flags; readonly IPAddress _ciaddr; readonly IPAddress _yiaddr; readonly IPAddress _siaddr; readonly IPAddress _giaddr; readonly byte[] _chaddr; readonly byte[] _sname; readonly byte[] _file; readonly IReadOnlyCollection _options; readonly byte[] _clientHardwareAddress; readonly string _serverHostName; readonly string _bootFileName; OptionOverloadOption _optionOverload; DhcpMessageTypeOption _dhcpMessageType; VendorClassIdentifierOption _vendorClassIdentifier; ClientIdentifierOption _clientIdentifier; ClientIdentifierOption _clientHardwareIdentifier; HostNameOption _hostName; ClientFullyQualifiedDomainNameOption _clientFullyQualifiedDomainName; ParameterRequestListOption _parameterRequestList; MaximumDhcpMessageSizeOption _maximumDhcpMessageSize; ServerIdentifierOption _serverIdentifier; RequestedIpAddressOption _requestedIpAddress; #endregion #region constructor public DhcpMessage(DhcpMessageOpCode op, DhcpMessageHardwareAddressType hardwareAddressType, byte[] xid, byte[] secs, DhcpMessageFlags flags, IPAddress ciaddr, IPAddress yiaddr, IPAddress siaddr, IPAddress giaddr, byte[] clientHardwareAddress, string sname, string file, IReadOnlyCollection options) { if (ciaddr.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("Address family not supported.", nameof(ciaddr)); if (yiaddr.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("Address family not supported.", nameof(yiaddr)); if (siaddr.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("Address family not supported.", nameof(siaddr)); if (giaddr.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("Address family not supported.", nameof(giaddr)); ArgumentNullException.ThrowIfNull(clientHardwareAddress); if (clientHardwareAddress.Length > 16) throw new ArgumentException("Client hardware address cannot exceed 16 bytes.", nameof(clientHardwareAddress)); if (xid.Length != 4) throw new ArgumentException("Transaction ID must be 4 bytes.", nameof(xid)); if (secs.Length != 2) throw new ArgumentException("Seconds elapsed must be 2 bytes.", nameof(secs)); _op = op; _htype = hardwareAddressType; _hlen = Convert.ToByte(clientHardwareAddress.Length); _hops = 0; _xid = xid; _secs = secs; _flags = flags; _ciaddr = ciaddr; _yiaddr = yiaddr; _siaddr = siaddr; _giaddr = giaddr; _clientHardwareAddress = clientHardwareAddress; _chaddr = new byte[16]; Buffer.BlockCopy(_clientHardwareAddress, 0, _chaddr, 0, _clientHardwareAddress.Length); _sname = new byte[64]; if (sname != null) { _serverHostName = sname; byte[] buffer = Encoding.ASCII.GetBytes(sname); if (buffer.Length >= 64) throw new ArgumentException("Server host name cannot exceed 63 bytes.", nameof(sname)); Buffer.BlockCopy(buffer, 0, _sname, 0, buffer.Length); } _file = new byte[128]; if (file != null) { _bootFileName = file; byte[] buffer = Encoding.ASCII.GetBytes(file); if (buffer.Length >= 128) throw new ArgumentException("Boot file name cannot exceed 127 bytes.", nameof(file)); Buffer.BlockCopy(buffer, 0, _file, 0, buffer.Length); } _options = options; foreach (DhcpOption option in _options) { if (option.Code == DhcpOptionCode.ServerIdentifier) { _serverIdentifier = option as ServerIdentifierOption; break; } } } public DhcpMessage(Stream s) { Span buffer = stackalloc byte[4]; s.ReadExactly(buffer); _op = (DhcpMessageOpCode)buffer[0]; _htype = (DhcpMessageHardwareAddressType)buffer[1]; _hlen = buffer[2]; _hops = buffer[3]; _xid = s.ReadExactly(4); s.ReadExactly(buffer); _secs = new byte[2]; buffer.Slice(0, 2).CopyTo(_secs); buffer.Reverse(); _flags = (DhcpMessageFlags)BitConverter.ToUInt16(buffer); s.ReadExactly(buffer); _ciaddr = new IPAddress(buffer); s.ReadExactly(buffer); _yiaddr = new IPAddress(buffer); s.ReadExactly(buffer); _siaddr = new IPAddress(buffer); s.ReadExactly(buffer); _giaddr = new IPAddress(buffer); _chaddr = s.ReadExactly(16); _clientHardwareAddress = new byte[_hlen]; Buffer.BlockCopy(_chaddr, 0, _clientHardwareAddress, 0, _hlen); _sname = s.ReadExactly(64); _file = s.ReadExactly(128); //read options List options = new List(); _options = options; s.ReadExactly(buffer); uint magicCookie = BitConverter.ToUInt32(buffer); if (magicCookie == MAGIC_COOKIE) { ParseOptions(s, options); if ((_optionOverload != null) && _optionOverload.Value.HasFlag(OptionOverloadValue.FileFieldUsed)) { using (MemoryStream mS = new MemoryStream(_file)) { ParseOptions(mS, options); } } else { for (int i = 0; i < _file.Length; i++) { if (_file[i] == 0) { if (i == 0) break; _bootFileName = Encoding.ASCII.GetString(_file, 0, i); break; } } } if ((_optionOverload != null) && _optionOverload.Value.HasFlag(OptionOverloadValue.SnameFieldUsed)) { using (MemoryStream mS = new MemoryStream(_sname)) { ParseOptions(mS, options); } } else { for (int i = 0; i < _sname.Length; i++) { if (_sname[i] == 0) { if (i == 0) break; _serverHostName = Encoding.ASCII.GetString(_sname, 0, i); break; } } } //parse all option values foreach (DhcpOption option in options) option.ParseOptionValue(); } if (_maximumDhcpMessageSize != null) _maximumDhcpMessageSize = new MaximumDhcpMessageSizeOption(576); } #endregion #region static public static DhcpMessage CreateReply(DhcpMessage request, IPAddress yiaddr, IPAddress siaddr, string sname, string file, IReadOnlyCollection options) { return new DhcpMessage(DhcpMessageOpCode.BootReply, request.HardwareAddressType, request.TransactionId, request.SecondsElapsed, request.Flags, request.ClientIpAddress, yiaddr, siaddr, request.RelayAgentIpAddress, request.ClientHardwareAddress, sname, file, options); } #endregion #region private private void ParseOptions(Stream s, List options) { while (true) { DhcpOption option = DhcpOption.Parse(s); if (option.Code == DhcpOptionCode.End) break; if (option.Code == DhcpOptionCode.Pad) continue; bool optionExists = false; foreach (DhcpOption existingOption in options) { if (existingOption.Code == option.Code) { //option already exists so append current option value into existing option existingOption.AppendOptionValue(option); optionExists = true; break; } } if (optionExists) continue; //add option to list options.Add(option); switch (option.Code) { case DhcpOptionCode.DhcpMessageType: _dhcpMessageType = option as DhcpMessageTypeOption; break; case DhcpOptionCode.VendorClassIdentifier: _vendorClassIdentifier = option as VendorClassIdentifierOption; break; case DhcpOptionCode.ClientIdentifier: _clientIdentifier = option as ClientIdentifierOption; break; case DhcpOptionCode.HostName: _hostName = option as HostNameOption; break; case DhcpOptionCode.ClientFullyQualifiedDomainName: _clientFullyQualifiedDomainName = option as ClientFullyQualifiedDomainNameOption; break; case DhcpOptionCode.ParameterRequestList: _parameterRequestList = option as ParameterRequestListOption; break; case DhcpOptionCode.MaximumDhcpMessageSize: _maximumDhcpMessageSize = option as MaximumDhcpMessageSizeOption; break; case DhcpOptionCode.ServerIdentifier: _serverIdentifier = option as ServerIdentifierOption; break; case DhcpOptionCode.RequestedIpAddress: _requestedIpAddress = option as RequestedIpAddressOption; break; case DhcpOptionCode.OptionOverload: _optionOverload = option as OptionOverloadOption; break; } } } #endregion #region public public void WriteTo(Stream s) { s.WriteByte((byte)_op); s.WriteByte((byte)_htype); s.WriteByte(_hlen); s.WriteByte(_hops); s.Write(_xid); s.Write(_secs); byte[] buffer = BitConverter.GetBytes((ushort)_flags); Array.Reverse(buffer); s.Write(buffer); s.Write(_ciaddr.GetAddressBytes()); s.Write(_yiaddr.GetAddressBytes()); s.Write(_siaddr.GetAddressBytes()); s.Write(_giaddr.GetAddressBytes()); s.Write(_chaddr); s.Write(_sname); s.Write(_file); //write options s.Write(BitConverter.GetBytes(MAGIC_COOKIE)); foreach (DhcpOption option in _options) option.WriteTo(s); } public ClientIdentifierOption GetClientIdentifier(bool ignoreClientIdentifierOption) { if (ignoreClientIdentifierOption || (_clientIdentifier is null)) { if (_clientHardwareIdentifier is null) _clientHardwareIdentifier = new ClientIdentifierOption((byte)_htype, _clientHardwareAddress); return _clientHardwareIdentifier; } return _clientIdentifier; } public string GetClientFullIdentifier() { string hardwareAddress = BitConverter.ToString(_clientHardwareAddress); if (_clientFullyQualifiedDomainName != null) return _clientFullyQualifiedDomainName.DomainName + " [" + hardwareAddress + "]"; if (_hostName != null) return _hostName.HostName + " [" + hardwareAddress + "]"; return "[" + hardwareAddress + "]"; } #endregion #region properties public DhcpMessageOpCode OpCode { get { return _op; } } public DhcpMessageHardwareAddressType HardwareAddressType { get { return _htype; } } public byte HardwareAddressLength { get { return _hlen; } } public byte Hops { get { return _hops; } } public byte[] TransactionId { get { return _xid; } } public byte[] SecondsElapsed { get { return _secs; } } public DhcpMessageFlags Flags { get { return _flags; } } public IPAddress ClientIpAddress { get { return _ciaddr; } } public IPAddress YourClientIpAddress { get { return _yiaddr; } } public IPAddress NextServerIpAddress { get { return _siaddr; } } public IPAddress RelayAgentIpAddress { get { return _giaddr; } } public byte[] ClientHardwareAddress { get { return _clientHardwareAddress; } } public string ServerHostName { get { return _serverHostName; } } public string BootFileName { get { return _bootFileName; } } public IReadOnlyCollection Options { get { return _options; } } public DhcpMessageTypeOption DhcpMessageType { get { return _dhcpMessageType; } } public VendorClassIdentifierOption VendorClassIdentifier { get { return _vendorClassIdentifier; } } public HostNameOption HostName { get { return _hostName; } } public ClientFullyQualifiedDomainNameOption ClientFullyQualifiedDomainName { get { return _clientFullyQualifiedDomainName; } } public ParameterRequestListOption ParameterRequestList { get { return _parameterRequestList; } } public MaximumDhcpMessageSizeOption MaximumDhcpMessageSize { get { return _maximumDhcpMessageSize; } } public ServerIdentifierOption ServerIdentifier { get { return _serverIdentifier; } } public RequestedIpAddressOption RequestedIpAddress { get { return _requestedIpAddress; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/DhcpOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.Dhcp.Options; using System; using System.IO; using TechnitiumLibrary; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp { public enum DhcpOptionCode : byte { Pad = 0, SubnetMask = 1, TimeOffset = 2, Router = 3, TimeServer = 4, NameServer = 5, DomainNameServer = 6, LogServer = 7, CookieServer = 8, LprServer = 9, ImpressServer = 10, ResourceLocationServer = 11, HostName = 12, BootFileSize = 13, MeritDump = 14, DomainName = 15, SwapServer = 16, RootPath = 17, ExtensionPath = 18, IpForwarding = 19, NonLocalSourceRouting = 20, PolicyFilter = 21, MaximumDatagramReassemblySize = 22, DefaultIpTtl = 23, PathMtuAgingTimeout = 24, PathMtuPlateauTable = 25, InterfaceMtu = 26, AllSubnetAreLocal = 27, BroadcastAddress = 28, PerformMaskDiscovery = 29, MaskSupplier = 30, PerformRouterDiscovery = 31, RouterSolicitationAddress = 32, StaticRoute = 33, TrailerEncapsulation = 34, ArpCacheTimeout = 35, EthernetEncapsulation = 36, TcpDefaultTtl = 37, TcpKeepAliveInterval = 38, TcpKeepAliveGarbage = 39, NetworkInformationServiceDomain = 40, NetworkInformationServers = 41, NetworkTimeProtocolServers = 42, VendorSpecificInformation = 43, NetBiosOverTcpIpNameServer = 44, NetBiosOverTcpIpDatagramDistributionServer = 45, NetBiosOverTcpIpNodeType = 46, NetBiosOverTcpIpScope = 47, XWindowSystemFontServer = 48, XWindowSystemDisplayManager = 49, RequestedIpAddress = 50, IpAddressLeaseTime = 51, OptionOverload = 52, DhcpMessageType = 53, ServerIdentifier = 54, ParameterRequestList = 55, Message = 56, MaximumDhcpMessageSize = 57, RenewalTimeValue = 58, RebindingTimeValue = 59, VendorClassIdentifier = 60, ClientIdentifier = 61, NetworkInformationServicePlusDomain = 64, NetworkInformationServicePlusServers = 65, TftpServerName = 66, BootfileName = 67, MobileIpHomeAgent = 68, SmtpServer = 69, Pop3Server = 70, NntpServer = 71, DefaultWwwServer = 72, DefaultFingerServer = 73, DefaultIrc = 74, StreetTalkServer = 75, StreetTalkDirectoryAssistance = 76, ClientFullyQualifiedDomainName = 81, DomainSearch = 119, ClasslessStaticRoute = 121, CAPWAPAccessControllerAddresses = 138, TftpServerAddress = 150, End = 255 } public class DhcpOption { #region variables readonly DhcpOptionCode _code; byte[] _value; #endregion #region constructor public DhcpOption(DhcpOptionCode code, string hexValue) { ArgumentNullException.ThrowIfNull(hexValue); _code = code; if (hexValue.Contains(':')) _value = hexValue.ParseColonHexString(); else _value = Convert.FromHexString(hexValue); } public DhcpOption(DhcpOptionCode code, byte[] value) { ArgumentNullException.ThrowIfNull(value); _code = code; _value = value; } protected DhcpOption(DhcpOptionCode code, Stream s) { _code = code; int len = s.ReadByte(); if (len < 0) throw new EndOfStreamException(); _value = s.ReadExactly(len); } protected DhcpOption(DhcpOptionCode code) { _code = code; } #endregion #region static public static DhcpOption CreateEndOption() { return new DhcpOption(DhcpOptionCode.End); } public static DhcpOption Parse(Stream s) { int code = s.ReadByte(); if (code < 0) throw new EndOfStreamException(); DhcpOptionCode optionCode = (DhcpOptionCode)code; switch (optionCode) { case DhcpOptionCode.SubnetMask: return new SubnetMaskOption(s); case DhcpOptionCode.Router: return new RouterOption(s); case DhcpOptionCode.DomainNameServer: return new DomainNameServerOption(s); case DhcpOptionCode.HostName: return new HostNameOption(s); case DhcpOptionCode.DomainName: return new DomainNameOption(s); case DhcpOptionCode.BroadcastAddress: return new BroadcastAddressOption(s); case DhcpOptionCode.VendorSpecificInformation: return new VendorSpecificInformationOption(s); case DhcpOptionCode.NetBiosOverTcpIpNameServer: return new NetBiosNameServerOption(s); case DhcpOptionCode.RequestedIpAddress: return new RequestedIpAddressOption(s); case DhcpOptionCode.IpAddressLeaseTime: return new IpAddressLeaseTimeOption(s); case DhcpOptionCode.OptionOverload: return new OptionOverloadOption(s); case DhcpOptionCode.DhcpMessageType: return new DhcpMessageTypeOption(s); case DhcpOptionCode.ServerIdentifier: return new ServerIdentifierOption(s); case DhcpOptionCode.ParameterRequestList: return new ParameterRequestListOption(s); case DhcpOptionCode.MaximumDhcpMessageSize: return new MaximumDhcpMessageSizeOption(s); case DhcpOptionCode.RenewalTimeValue: return new RenewalTimeValueOption(s); case DhcpOptionCode.RebindingTimeValue: return new RebindingTimeValueOption(s); case DhcpOptionCode.VendorClassIdentifier: return new VendorClassIdentifierOption(s); case DhcpOptionCode.ClientIdentifier: return new ClientIdentifierOption(s); case DhcpOptionCode.ClientFullyQualifiedDomainName: return new ClientFullyQualifiedDomainNameOption(s); case DhcpOptionCode.DomainSearch: return new DomainSearchOption(s); case DhcpOptionCode.ClasslessStaticRoute: return new ClasslessStaticRouteOption(s); case DhcpOptionCode.CAPWAPAccessControllerAddresses: return new CAPWAPAccessControllerOption(s); case DhcpOptionCode.TftpServerAddress: return new TftpServerAddressOption(s); case DhcpOptionCode.Pad: case DhcpOptionCode.End: return new DhcpOption(optionCode); default: //unknown option return new DhcpOption(optionCode, s); } } #endregion #region internal internal void AppendOptionValue(DhcpOption option) { byte[] value = new byte[_value.Length + option._value.Length]; Buffer.BlockCopy(_value, 0, value, 0, _value.Length); Buffer.BlockCopy(option._value, 0, value, _value.Length, option._value.Length); _value = value; } internal void ParseOptionValue() { if (_value != null) { using (MemoryStream mS = new MemoryStream(_value)) { ParseOptionValue(mS); } } } #endregion #region protected protected virtual void ParseOptionValue(Stream s) { } protected virtual void WriteOptionValue(Stream s) { if (_value == null) throw new NotImplementedException(); s.Write(_value); } #endregion #region public public void WriteTo(Stream s) { switch (_code) { case DhcpOptionCode.Pad: case DhcpOptionCode.End: s.WriteByte((byte)_code); break; default: using (MemoryStream mS = new MemoryStream()) { WriteOptionValue(mS); int len = 255; int valueLen = Convert.ToInt32(mS.Position); mS.Position = 0; do { if (valueLen < len) len = valueLen; //write option s.WriteByte((byte)_code); //code s.WriteByte((byte)len); //len mS.CopyTo(s, len, len); //value valueLen -= len; } while (valueLen > 0); } break; } } #endregion #region properties public DhcpOptionCode Code { get { return _code; } } public byte[] RawValue { get { return _value; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/DhcpServer.cs ================================================ /* 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.Dhcp.Options; using DnsServerCore.Dns; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Zones; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dhcp { //Dynamic Host Configuration Protocol //https://datatracker.ietf.org/doc/html/rfc2131 //DHCP Options and BOOTP Vendor Extensions //https://datatracker.ietf.org/doc/html/rfc2132 //Encoding Long Options in the Dynamic Host Configuration Protocol (DHCPv4) //https://datatracker.ietf.org/doc/html/rfc3396 //Client Fully Qualified Domain Name(FQDN) Option //https://datatracker.ietf.org/doc/html/rfc4702 public sealed class DhcpServer : IDisposable { #region enum enum ServiceState { Stopped = 0, Starting = 1, Running = 2, Stopping = 3 } #endregion #region variables readonly string _scopesFolder; readonly LogManager _log; readonly ConcurrentDictionary _udpListeners = new ConcurrentDictionary(); readonly ConcurrentDictionary _scopes = new ConcurrentDictionary(); DnsServer _dnsServer; AuthManager _authManager; volatile ServiceState _state = ServiceState.Stopped; readonly IPEndPoint _dhcpDefaultEP = new IPEndPoint(IPAddress.Any, 67); Timer _maintenanceTimer; const int MAINTENANCE_TIMER_INTERVAL = 10000; DateTime _lastModifiedScopesSavedOn; #endregion #region constructor public DhcpServer(string scopesFolder, LogManager log) { _scopesFolder = scopesFolder; _log = log; if (!Directory.Exists(_scopesFolder)) { Directory.CreateDirectory(_scopesFolder); //create default scope Scope scope = new Scope("Default", false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.254"), IPAddress.Parse("255.255.255.0"), _log, this); scope.Exclusions = new Exclusion[] { new Exclusion(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.10")) }; scope.RouterAddress = IPAddress.Parse("192.168.1.1"); scope.UseThisDnsServer = true; scope.DomainName = "home"; scope.LeaseTimeDays = 1; scope.IgnoreClientIdentifierOption = true; SaveScopeFile(scope); } } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _maintenanceTimer?.Dispose(); Stop(); if (_scopes is not null) { foreach (KeyValuePair scope in _scopes) scope.Value.Dispose(); _scopes.Clear(); } _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private async Task ReadUdpRequestAsync(Socket udpListener) { byte[] recvBuffer = new byte[1500]; try { bool processOnlyUnicastMessages = !(udpListener.LocalEndPoint as IPEndPoint).Address.Equals(IPAddress.Any); //only 0.0.0.0 ip should process broadcast to avoid duplicate offers on Windows EndPoint epAny = new IPEndPoint(IPAddress.Any, 0); SocketReceiveMessageFromResult result; while (true) { try { result = await udpListener.ReceiveMessageFromAsync(recvBuffer, SocketFlags.None, epAny); } catch (SocketException ex) { switch (ex.SocketErrorCode) { case SocketError.ConnectionReset: case SocketError.HostUnreachable: case SocketError.NetworkReset: result = default; break; case SocketError.MessageSize: _log.Write(ex); result = default; break; default: throw; } } if (result.ReceivedBytes > 0) { if (processOnlyUnicastMessages && result.PacketInformation.Address.Equals(IPAddress.Broadcast)) continue; try { DhcpMessage request = new DhcpMessage(new MemoryStream(recvBuffer, 0, result.ReceivedBytes, false)); _ = ProcessDhcpRequestAsync(request, result.RemoteEndPoint as IPEndPoint, result.PacketInformation, udpListener); } catch (Exception ex) { _log.Write(result.RemoteEndPoint as IPEndPoint, ex); } } } } catch (ObjectDisposedException) { //server stopped } catch (SocketException ex) { switch (ex.SocketErrorCode) { case SocketError.OperationAborted: case SocketError.Interrupted: break; //server stopping default: if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(ex); break; } } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(ex); } } private async Task ProcessDhcpRequestAsync(DhcpMessage request, IPEndPoint remoteEP, IPPacketInformation ipPacketInformation, Socket udpListener) { try { DhcpMessage response = await ProcessDhcpMessageAsync(request, remoteEP, ipPacketInformation); //send response if (response != null) { byte[] sendBuffer = new byte[1024]; MemoryStream sendBufferStream = new MemoryStream(sendBuffer); response.WriteTo(sendBufferStream); //send dns datagram if (!request.RelayAgentIpAddress.Equals(IPAddress.Any)) { //received request via relay agent so send unicast response to relay agent on port 67 await udpListener.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, new IPEndPoint(request.RelayAgentIpAddress, 67)); } else if (!request.ClientIpAddress.Equals(IPAddress.Any)) { //client is already configured and renewing lease so send unicast response on port 68 await udpListener.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, new IPEndPoint(request.ClientIpAddress, 68)); } else { Socket udpSocket; //send response as broadcast on port 68 on appropriate interface bound socket if (_udpListeners.TryGetValue(response.ServerIdentifier.Address, out UdpListener listener)) udpSocket = listener.Socket; //found scope specific socket else udpSocket = udpListener; //no appropriate socket found so use default socket await udpSocket.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.DontRoute, new IPEndPoint(IPAddress.Broadcast, 68)); //no routing for broadcast } } } catch (ObjectDisposedException) { //socket disposed } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(remoteEP, ex); } } private async Task ProcessDhcpMessageAsync(DhcpMessage request, IPEndPoint remoteEP, IPPacketInformation ipPacketInformation) { if (request.OpCode != DhcpMessageOpCode.BootRequest) return null; switch (request.DhcpMessageType?.Type) { case DhcpMessageType.Discover: { Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation); if (scope == null) return null; //no scope available; do nothing if ((request.ServerHostName != null) && (request.ServerHostName != scope.ServerHostName)) return null; //discard request; since this request is for another server with the specified server host name if ((request.BootFileName != null) && (request.BootFileName != scope.BootFileName)) return null; //discard request; since this request wants boot file not available on this server if (scope.OfferDelayTime > 0) await Task.Delay(scope.OfferDelayTime); //delay sending offer Lease offer = await scope.GetOfferAsync(request); if (offer == null) return null; //no offer available, do nothing IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress; string reservedLeaseHostName = null; if (!string.IsNullOrWhiteSpace(scope.DomainName)) { //get override host name from reserved lease Lease reservedLease = scope.GetReservedLease(request); if (reservedLease is not null) reservedLeaseHostName = reservedLease.HostName; } List options = await scope.GetOptionsAsync(request, serverIdentifierAddress, reservedLeaseHostName, _dnsServer); if (options is null) return null; //log ip offer _log.Write(remoteEP, "DHCP Server offered IP address [" + offer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); return DhcpMessage.CreateReply(request, offer.Address, scope.ServerAddress ?? serverIdentifierAddress, scope.ServerHostName, scope.BootFileName, options); } case DhcpMessageType.Request: { //request ip address lease or extend existing lease Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation); if (scope == null) return null; //no scope available; do nothing IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress; Lease leaseOffer; if (request.ServerIdentifier == null) { if (request.RequestedIpAddress == null) { //renewing or rebinding if (request.ClientIpAddress.Equals(IPAddress.Any)) return null; //client must set IP address in ciaddr; do nothing leaseOffer = scope.GetExistingLeaseOrOffer(request); if (leaseOffer == null) { //no existing lease or offer available for client //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } if (!request.ClientIpAddress.Equals(leaseOffer.Address)) { //client ip is incorrect //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } } else { //init-reboot leaseOffer = scope.GetExistingLeaseOrOffer(request); if (leaseOffer == null) { //no existing lease or offer available for client //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } if (!request.RequestedIpAddress.Address.Equals(leaseOffer.Address)) { //the client's notion of its IP address is not correct - RFC 2131 //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } } if ((leaseOffer.Type == LeaseType.Dynamic) && (scope.IsAddressExcluded(leaseOffer.Address) || scope.IsAddressReserved(leaseOffer.Address))) { //client ip is excluded/reserved for dynamic allocations scope.ReleaseLease(leaseOffer); //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } Lease reservedLease = scope.GetReservedLease(request); if (reservedLease == null) { if (leaseOffer.Type == LeaseType.Reserved) { //client's reserved lease has been removed so release the current lease and send NAK to allow it to get new allocation scope.ReleaseLease(leaseOffer); //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } } else { if (!reservedLease.Address.Equals(leaseOffer.Address)) { //client has a new reserved lease so release the current lease and send NAK to allow it to get new allocation scope.ReleaseLease(leaseOffer); //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } } } else { //selecting offer if (request.RequestedIpAddress == null) return null; //client MUST include this option; do nothing if (!request.ServerIdentifier.Address.Equals(serverIdentifierAddress)) return null; //offer declined by client; do nothing leaseOffer = scope.GetExistingLeaseOrOffer(request); if (leaseOffer == null) { //no existing lease or offer available for client //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } if (!request.RequestedIpAddress.Address.Equals(leaseOffer.Address)) { //requested ip is incorrect //send nak return DhcpMessage.CreateReply(request, IPAddress.Any, IPAddress.Any, null, null, new DhcpOption[] { new DhcpMessageTypeOption(DhcpMessageType.Nak), new ServerIdentifierOption(scope.InterfaceAddress), DhcpOption.CreateEndOption() }); } } string reservedLeaseHostName = null; if (!string.IsNullOrWhiteSpace(scope.DomainName)) { //get override host name from reserved lease Lease reservedLease = scope.GetReservedLease(request); if (reservedLease is not null) reservedLeaseHostName = reservedLease.HostName; } List options = await scope.GetOptionsAsync(request, serverIdentifierAddress, reservedLeaseHostName, _dnsServer); if (options is null) return null; scope.CommitLease(leaseOffer); //log ip lease _log.Write(remoteEP, "DHCP Server leased IP address [" + leaseOffer.Address.ToString() + "] to " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); if (string.IsNullOrWhiteSpace(scope.DomainName)) { //update lease hostname leaseOffer.SetHostName(request.HostName?.HostName); } else { //update dns string clientDomainName = null; if (!string.IsNullOrWhiteSpace(reservedLeaseHostName)) clientDomainName = GetSanitizedHostName(reservedLeaseHostName) + "." + scope.DomainName; if (string.IsNullOrWhiteSpace(clientDomainName)) { foreach (DhcpOption option in options) { if (option.Code == DhcpOptionCode.ClientFullyQualifiedDomainName) { clientDomainName = (option as ClientFullyQualifiedDomainNameOption).DomainName; break; } } } if (string.IsNullOrWhiteSpace(clientDomainName)) { if ((request.HostName is not null) && !string.IsNullOrWhiteSpace(request.HostName.HostName)) clientDomainName = GetSanitizedHostName(request.HostName.HostName) + "." + scope.DomainName; } if (!string.IsNullOrWhiteSpace(clientDomainName)) { if (!clientDomainName.Equals(leaseOffer.HostName, StringComparison.OrdinalIgnoreCase)) UpdateDnsAuthZone(false, scope, leaseOffer); //hostname changed! delete old hostname entry from DNS leaseOffer.SetHostName(clientDomainName); UpdateDnsAuthZone(true, scope, leaseOffer); } } return DhcpMessage.CreateReply(request, leaseOffer.Address, scope.ServerAddress ?? serverIdentifierAddress, scope.ServerHostName, scope.BootFileName, options); } case DhcpMessageType.Decline: { //ip address is already in use as detected by client via ARP if ((request.ServerIdentifier == null) || (request.RequestedIpAddress == null)) return null; //client MUST include these option; do nothing Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation); if (scope == null) return null; //no scope available; do nothing IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress; if (!request.ServerIdentifier.Address.Equals(serverIdentifierAddress)) return null; //request not for this server; do nothing Lease lease = scope.GetExistingLeaseOrOffer(request); if (lease == null) return null; //no existing lease or offer available for client; do nothing if (!lease.Address.Equals(request.RequestedIpAddress.Address)) return null; //the client's notion of its IP address is not correct; do nothing //remove lease since the IP address is used by someone else scope.ReleaseLease(lease); //log issue _log.Write(remoteEP, "DHCP Server received DECLINE message for scope '" + scope.Name + "': " + lease.GetClientInfo() + " detected that IP address [" + lease.Address + "] is already in use."); //update dns UpdateDnsAuthZone(false, scope, lease); //do nothing return null; } case DhcpMessageType.Release: { //cancel ip address lease if (request.ServerIdentifier == null) return null; //client MUST include this option; do nothing Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation); if (scope == null) return null; //no scope available; do nothing IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress; if (!request.ServerIdentifier.Address.Equals(serverIdentifierAddress)) return null; //request not for this server; do nothing Lease lease = scope.GetExistingLeaseOrOffer(request); if (lease == null) return null; //no existing lease or offer available for client; do nothing if (!lease.Address.Equals(request.ClientIpAddress)) return null; //the client's notion of its IP address is not correct; do nothing //release lease scope.ReleaseLease(lease); //log ip lease release _log.Write(remoteEP, "DHCP Server released IP address [" + lease.Address.ToString() + "] that was leased to " + lease.GetClientInfo() + " for scope: " + scope.Name); //update dns UpdateDnsAuthZone(false, scope, lease); //do nothing return null; } case DhcpMessageType.Inform: { //need only local config; already has ip address assigned externally/manually Scope scope = FindScope(request, remoteEP.Address, ipPacketInformation); if (scope == null) return null; //no scope available; do nothing IPAddress serverIdentifierAddress = scope.InterfaceAddress.Equals(IPAddress.Any) ? ipPacketInformation.Address : scope.InterfaceAddress; //log inform _log.Write(remoteEP, "DHCP Server received INFORM message from " + request.GetClientFullIdentifier() + " for scope: " + scope.Name); List options = await scope.GetOptionsAsync(request, serverIdentifierAddress, null, _dnsServer); if (options is null) return null; if (!string.IsNullOrWhiteSpace(scope.DomainName)) { //update dns string clientDomainName = null; foreach (DhcpOption option in options) { if (option.Code == DhcpOptionCode.ClientFullyQualifiedDomainName) { clientDomainName = (option as ClientFullyQualifiedDomainNameOption).DomainName; break; } } if (string.IsNullOrWhiteSpace(clientDomainName)) { if (request.HostName is not null) clientDomainName = GetSanitizedHostName(request.HostName.HostName) + "." + scope.DomainName; } if (!string.IsNullOrWhiteSpace(clientDomainName)) UpdateDnsAuthZone(true, scope, clientDomainName, request.ClientIpAddress, false); } return DhcpMessage.CreateReply(request, IPAddress.Any, scope.ServerAddress ?? serverIdentifierAddress, null, null, options); } default: return null; } } private Scope FindScope(DhcpMessage request, IPAddress remoteAddress, IPPacketInformation ipPacketInformation) { if (request.RelayAgentIpAddress.Equals(IPAddress.Any)) { //no relay agent if (request.ClientIpAddress.Equals(IPAddress.Any)) { if (!ipPacketInformation.Address.Equals(IPAddress.Broadcast)) return null; //message destination address must be broadcast address //broadcast request Scope foundScope = null; foreach (KeyValuePair entry in _scopes) { Scope scope = entry.Value; if (scope.Enabled && (scope.InterfaceIndex == ipPacketInformation.Interface)) { if (scope.GetReservedLease(request) != null) return scope; //found reserved lease on this scope if ((foundScope == null) && !scope.AllowOnlyReservedLeases) foundScope = scope; } } return foundScope; } else { if ((request.DhcpMessageType?.Type != DhcpMessageType.Decline) && !remoteAddress.Equals(request.ClientIpAddress)) return null; //client ip must match udp src addr //unicast request foreach (KeyValuePair entry in _scopes) { Scope scope = entry.Value; if (scope.Enabled && scope.IsAddressInRange(request.ClientIpAddress)) return scope; } return null; } } else { //relay agent unicast Scope foundScope = null; foreach (KeyValuePair entry in _scopes) { Scope scope = entry.Value; if (scope.Enabled && scope.InterfaceAddress.Equals(IPAddress.Any) && scope.IsAddressInNetwork(request.RelayAgentIpAddress)) { if (scope.GetReservedLease(request) != null) return scope; //found reserved lease on this scope if (!request.ClientIpAddress.Equals(IPAddress.Any) && scope.IsAddressInRange(request.ClientIpAddress)) return scope; //client IP address is in scope range if ((foundScope == null) && !scope.AllowOnlyReservedLeases) foundScope = scope; } } return foundScope; } } internal static string GetSanitizedHostName(string hostname) { StringBuilder sb = new StringBuilder(hostname.Length); foreach (char c in hostname) { if ((c >= 97) && (c <= 122)) //[a-z] sb.Append(c); else if ((c >= 65) && (c <= 90)) //[A-Z] sb.Append(c); else if ((c >= 48) && (c <= 57)) //[0-9] sb.Append(c); else if (c == 45) //[-] sb.Append(c); else if (c == 95) //[_] sb.Append(c); else if (c == '.') sb.Append(c); else if (c == ' ') sb.Append('-'); } return sb.ToString(); } internal void UpdateDnsAuthZone(bool add, Scope scope, Lease lease) { UpdateDnsAuthZone(add, scope, lease.HostName, lease.Address, lease.Type == LeaseType.Reserved); } private void UpdateDnsAuthZone(bool add, Scope scope, string domain, IPAddress address, bool isReservedLease) { if ((_dnsServer is null) || (_authManager is null)) return; if (string.IsNullOrWhiteSpace(scope.DomainName) || !scope.DnsUpdates) return; if (string.IsNullOrWhiteSpace(domain)) return; if (!DnsClient.IsDomainNameValid(domain)) return; if (!domain.EndsWith("." + scope.DomainName, StringComparison.OrdinalIgnoreCase)) return; //domain does not end with scope domain name try { string zoneName = null; string reverseDomain = Zone.GetReverseZone(address, 32); string reverseZoneName = null; if (add) { //update forward zone AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(scope.DomainName); if (zoneInfo is null) { //zone does not exists; create new primary zone zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(scope.DomainName); if (zoneInfo is null) { _log.Write("DHCP Server failed to create DNS primary zone '" + scope.DomainName + "'."); return; } //set permissions _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); _log.Write("DHCP Server create DNS primary zone '" + zoneInfo.DisplayName + "'."); } else if ((zoneInfo.Type != AuthZoneType.Primary) && (zoneInfo.Type != AuthZoneType.Forwarder)) { if (zoneInfo.Name.Equals(scope.DomainName, StringComparison.OrdinalIgnoreCase)) throw new DhcpServerException("Cannot update DNS zone '" + zoneInfo.DisplayName + "': not a primary or a forwarder zone."); //create new primary zone zoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(scope.DomainName); if (zoneInfo is null) { _log.Write("DHCP Server failed to create DNS primary zone '" + scope.DomainName + "'."); return; } //set permissions _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); _log.Write("DHCP Server create DNS primary zone '" + zoneInfo.DisplayName + "'."); } zoneName = zoneInfo.Name; if (!isReservedLease && !scope.DnsOverwriteForDynamicLease) { //check for existing record for the dynamic leases IReadOnlyList existingRecords = _dnsServer.AuthZoneManager.GetRecords(zoneName, domain, DnsResourceRecordType.A); if (existingRecords.Count > 0) { foreach (DnsResourceRecord existingRecord in existingRecords) { IPAddress existingAddress = (existingRecord.RDATA as DnsARecordData).Address; if (!existingAddress.Equals(address)) { //a DNS record already exists for the specified domain name with a different address //do not change DNS record for this dynamic lease _log.Write("DHCP Server cannot update DNS: an A record already exists for '" + domain + "' with a different IP address [" + existingAddress.ToString() + "]."); return; } } } } DnsResourceRecord aRecord = new DnsResourceRecord(domain, DnsResourceRecordType.A, DnsClass.IN, scope.DnsTtl, new DnsARecordData(address)); GenericRecordInfo aRecordInfo = aRecord.GetAuthGenericRecordInfo(); aRecordInfo.LastModified = DateTime.UtcNow; aRecordInfo.ExpiryTtl = scope.GetLeaseTime(); aRecordInfo.Comments = $"Via '{scope.Name}' DHCP scope"; _dnsServer.AuthZoneManager.SetRecord(zoneName, aRecord); _log.Write("DHCP Server updated DNS A record '" + domain + "' with IP address [" + address.ToString() + "]."); //update reverse zone AuthZoneInfo reverseZoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(reverseDomain); if (reverseZoneInfo is null) { string reverseZone = Zone.GetReverseZone(address, scope.SubnetMask); //reverse zone does not exists; create new reverse primary zone reverseZoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(reverseZone); if (reverseZoneInfo is null) { _log.Write("DHCP Server failed to create DNS primary zone '" + reverseZone + "'."); return; } //set permissions _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); _log.Write("DHCP Server create DNS primary zone '" + reverseZoneInfo.DisplayName + "'."); } else if ((reverseZoneInfo.Type != AuthZoneType.Primary) && (reverseZoneInfo.Type != AuthZoneType.Forwarder)) { string reverseZone = Zone.GetReverseZone(address, scope.SubnetMask); if (reverseZoneInfo.Name.Equals(reverseZone, StringComparison.OrdinalIgnoreCase)) throw new DhcpServerException("Cannot update reverse DNS zone '" + reverseZoneInfo.DisplayName + "': not a primary or a forwarder zone."); //create new reverse primary zone reverseZoneInfo = _dnsServer.AuthZoneManager.CreatePrimaryZone(reverseZone); if (reverseZoneInfo is null) { _log.Write("DHCP Server failed to create DNS primary zone '" + reverseZone + "'."); return; } //set permissions _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _authManager.GetGroup(Group.DHCP_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _authManager.SaveConfigFile(); _log.Write("DHCP Server create DNS primary zone '" + reverseZoneInfo.DisplayName + "'."); } reverseZoneName = reverseZoneInfo.Name; DnsResourceRecord ptrRecord = new DnsResourceRecord(reverseDomain, DnsResourceRecordType.PTR, DnsClass.IN, scope.DnsTtl, new DnsPTRRecordData(domain)); GenericRecordInfo ptrRecordInfo = aRecord.GetAuthGenericRecordInfo(); ptrRecordInfo.LastModified = DateTime.UtcNow; ptrRecordInfo.ExpiryTtl = scope.GetLeaseTime(); ptrRecordInfo.Comments = $"Via '{scope.Name}' DHCP scope"; _dnsServer.AuthZoneManager.SetRecord(reverseZoneName, ptrRecord); _log.Write("DHCP Server updated DNS PTR record '" + reverseDomain + "' with domain name '" + domain + "'."); } else { //remove from forward zone AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(domain); if ((zoneInfo is not null) && ((zoneInfo.Type == AuthZoneType.Primary) || (zoneInfo.Type == AuthZoneType.Forwarder))) { //primary zone exists zoneName = zoneInfo.Name; _dnsServer.AuthZoneManager.DeleteRecord(zoneName, domain, DnsResourceRecordType.A, new DnsARecordData(address)); _log.Write("DHCP Server deleted DNS A record '" + domain + "' with address [" + address.ToString() + "]."); } //remove from reverse zone AuthZoneInfo reverseZoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(reverseDomain); if ((reverseZoneInfo != null) && ((reverseZoneInfo.Type == AuthZoneType.Primary) || (reverseZoneInfo.Type == AuthZoneType.Forwarder))) { //primary reverse zone exists reverseZoneName = reverseZoneInfo.Name; _dnsServer.AuthZoneManager.DeleteRecord(reverseZoneName, reverseDomain, DnsResourceRecordType.PTR, new DnsPTRRecordData(domain)); _log.Write("DHCP Server deleted DNS PTR record '" + reverseDomain + "' with domain '" + domain + "'."); } } //save auth zone file if (zoneName is not null) _dnsServer?.AuthZoneManager.SaveZoneFile(zoneName); //save reverse auth zone file if (reverseZoneName is not null) _dnsServer?.AuthZoneManager.SaveZoneFile(reverseZoneName); } catch (Exception ex) { _log.Write(ex); } } private void BindUdpListener(IPEndPoint dhcpEP) { UdpListener listener = _udpListeners.GetOrAdd(dhcpEP.Address, delegate (IPAddress key) { Socket udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); try { #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom() if (Environment.OSVersion.Platform == PlatformID.Win32NT) { const uint IOC_IN = 0x80000000; const uint IOC_VENDOR = 0x18000000; const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; udpSocket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); } #endregion //bind to interface address if (Environment.OSVersion.Platform == PlatformID.Unix) udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses udpSocket.EnableBroadcast = true; udpSocket.ExclusiveAddressUse = false; udpSocket.Bind(dhcpEP); //start reading dhcp packets _ = Task.Factory.StartNew(delegate () { return ReadUdpRequestAsync(udpSocket); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current); return new UdpListener(udpSocket); } catch { udpSocket.Dispose(); throw; } }); listener.IncrementScopeCount(); } private bool UnbindUdpListener(IPEndPoint dhcpEP) { if (_udpListeners.TryGetValue(dhcpEP.Address, out UdpListener listener)) { listener.DecrementScopeCount(); if (listener.ScopeCount < 1) { if (_udpListeners.TryRemove(dhcpEP.Address, out _)) { listener.Socket.Dispose(); return true; } } } return false; } private async Task ActivateScopeAsync(Scope scope, bool waitForInterface, bool throwException = false) { IPEndPoint dhcpEP = null; try { //find scope interface for binding socket if (waitForInterface) { //retry for 30 seconds for interface to come up int tries = 0; while (true) { if (scope.FindInterface()) { if (!scope.InterfaceAddress.Equals(IPAddress.Any)) break; //break only when specific interface address is found } if (++tries >= 30) { if (scope.InterfaceAddress == null) throw new DhcpServerException("DHCP Server requires static IP address to work correctly but no network interface was found to have any static IP address configured."); break; //use the available ANY interface address } await Task.Delay(1000); } } else { if (!scope.FindInterface()) throw new DhcpServerException("DHCP Server requires static IP address to work correctly but no network interface was found to have any static IP address configured."); } //find this dns server address in case the network config has changed if (scope.UseThisDnsServer) scope.FindThisDnsServerAddress(); dhcpEP = new IPEndPoint(scope.InterfaceAddress, 67); if (!dhcpEP.Address.Equals(IPAddress.Any)) { int tries = 0; do { try { BindUdpListener(dhcpEP); break; } catch { if (!waitForInterface || (++tries >= 3)) throw; await Task.Delay(5000); } } while (waitForInterface); } try { BindUdpListener(_dhcpDefaultEP); } catch { if (!dhcpEP.Address.Equals(IPAddress.Any)) UnbindUdpListener(dhcpEP); throw; } if (_dnsServer is not null) { //update valid leases into dns DateTime utcNow = DateTime.UtcNow; foreach (KeyValuePair lease in scope.Leases) UpdateDnsAuthZone(utcNow < lease.Value.LeaseExpires, scope, lease.Value); //lease valid } _log.Write(dhcpEP, "DHCP Server successfully activated scope: " + scope.Name); return true; } catch (Exception ex) { _log.Write(dhcpEP, "DHCP Server failed to activate scope: " + scope.Name + "\r\n" + ex.ToString()); if (throwException) throw; } return false; } private bool DeactivateScope(Scope scope, bool throwException = false) { IPEndPoint dhcpEP = null; try { IPAddress interfaceAddress = scope.InterfaceAddress; dhcpEP = new IPEndPoint(interfaceAddress, 67); if (!interfaceAddress.Equals(IPAddress.Any)) UnbindUdpListener(dhcpEP); UnbindUdpListener(_dhcpDefaultEP); if (_dnsServer is not null) { //remove all leases from dns foreach (KeyValuePair lease in scope.Leases) UpdateDnsAuthZone(false, scope, lease.Value); } _log.Write(dhcpEP, "DHCP Server successfully deactivated scope: " + scope.Name); return true; } catch (Exception ex) { _log.Write(dhcpEP, "DHCP Server failed to deactivate scope: " + scope.Name + "\r\n" + ex.ToString()); if (throwException) throw; } return false; } private async Task LoadScopeAsync(Scope scope, bool waitForInterface) { foreach (KeyValuePair entry in _scopes) { Scope existingScope = entry.Value; if (existingScope.IsAddressInRange(scope.StartingAddress) || existingScope.IsAddressInRange(scope.EndingAddress)) throw new DhcpServerException("Scope with overlapping range already exists: " + existingScope.StartingAddress.ToString() + "-" + existingScope.EndingAddress.ToString()); } if (!_scopes.TryAdd(scope.Name, scope)) throw new DhcpServerException("Scope with same name already exists."); if (scope.Enabled) { if (!await ActivateScopeAsync(scope, waitForInterface)) scope.SetEnabled(false); } _log.Write("DHCP Server successfully loaded scope: " + scope.Name); } private void UnloadScope(Scope scope) { if (scope.Enabled) DeactivateScope(scope); if (_scopes.TryRemove(scope.Name, out Scope removedScope)) { removedScope.Dispose(); _log.Write("DHCP Server successfully unloaded scope: " + scope.Name); } } private void LoadAllScopeFiles() { string[] scopeFiles = Directory.GetFiles(_scopesFolder, "*.scope"); foreach (string scopeFile in scopeFiles) _ = LoadScopeFileAsync(scopeFile); _lastModifiedScopesSavedOn = DateTime.UtcNow; } private async Task LoadScopeFileAsync(string scopeFile) { //load scope file async to allow waiting for interface to come up try { using (FileStream fS = new FileStream(scopeFile, FileMode.Open, FileAccess.Read)) { await LoadScopeAsync(new Scope(fS, _log, this), true); } } catch (Exception ex) { _log.Write("DHCP Server failed to load scope file: " + scopeFile + "\r\n" + ex.ToString()); } } private void SaveScopeFile(Scope scope) { string scopeFile = Path.Combine(_scopesFolder, scope.Name + ".scope"); try { using (MemoryStream mS = new MemoryStream()) { //serialize scope scope.WriteTo(mS); //write config mS.Position = 0; using (FileStream fS = new FileStream(scopeFile, FileMode.Create, FileAccess.Write)) { mS.CopyTo(fS); } } _log.Write("DHCP Server successfully saved scope file: " + scopeFile); } catch (Exception ex) { _log.Write("DHCP Server failed to save scope file: " + scopeFile + "\r\n" + ex.ToString()); } } private void DeleteScopeFile(string scopeName) { string scopeFile = Path.Combine(_scopesFolder, scopeName + ".scope"); try { File.Delete(scopeFile); _log.Write("DHCP Server successfully deleted scope file: " + scopeFile); } catch (Exception ex) { _log.Write("DHCP Server failed to delete scope file: " + scopeFile + "\r\n" + ex.ToString()); } } private void SaveModifiedScopes() { DateTime currentDateTime = DateTime.UtcNow; foreach (KeyValuePair scope in _scopes) { if (scope.Value.LastModified > _lastModifiedScopesSavedOn) SaveScopeFile(scope.Value); } _lastModifiedScopesSavedOn = currentDateTime; } private void StartMaintenanceTimer() { if (_maintenanceTimer == null) { _maintenanceTimer = new Timer(delegate (object state) { try { foreach (KeyValuePair scope in _scopes) { scope.Value.RemoveExpiredOffers(); List expiredLeases = scope.Value.RemoveExpiredLeases(); if (expiredLeases.Count > 0) { _log.Write("DHCP Server removed " + expiredLeases.Count + " lease(s) from scope: " + scope.Value.Name); foreach (Lease expiredLease in expiredLeases) UpdateDnsAuthZone(false, scope.Value, expiredLease); } } SaveModifiedScopes(); } catch (Exception ex) { _log.Write(ex); } finally { try { _maintenanceTimer?.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } }, null, Timeout.Infinite, Timeout.Infinite); } _maintenanceTimer.Change(MAINTENANCE_TIMER_INTERVAL, Timeout.Infinite); } private void StopMaintenanceTimer() { _maintenanceTimer.Change(Timeout.Infinite, Timeout.Infinite); } #endregion #region public public void Start() { if (_disposed) ObjectDisposedException.ThrowIf(_disposed, this); if (_state != ServiceState.Stopped) throw new InvalidOperationException("DHCP Server is already running."); _state = ServiceState.Starting; LoadAllScopeFiles(); StartMaintenanceTimer(); _state = ServiceState.Running; } public void Stop() { if (_state != ServiceState.Running) return; _state = ServiceState.Stopping; StopMaintenanceTimer(); SaveModifiedScopes(); foreach (KeyValuePair scope in _scopes) UnloadScope(scope.Value); _udpListeners.Clear(); _state = ServiceState.Stopped; } public async Task AddScopeAsync(Scope scope) { await LoadScopeAsync(scope, false); SaveScopeFile(scope); } public Scope GetScope(string name) { if (_scopes.TryGetValue(name, out Scope scope)) return scope; return null; } public void RenameScope(string oldName, string newName) { Scope.ValidateScopeName(newName); if (!_scopes.TryGetValue(oldName, out Scope scope)) throw new DhcpServerException("Scope with name '" + oldName + "' does not exists."); if (!_scopes.TryAdd(newName, scope)) throw new DhcpServerException("Scope with name '" + newName + "' already exists."); scope.Name = newName; _scopes.TryRemove(oldName, out _); SaveScopeFile(scope); DeleteScopeFile(oldName); } public void DeleteScope(string name) { if (_scopes.TryGetValue(name, out Scope scope)) { UnloadScope(scope); DeleteScopeFile(scope.Name); } } public async Task EnableScopeAsync(string name, bool throwException = false) { if (_scopes.TryGetValue(name, out Scope scope)) { if (!scope.Enabled && await ActivateScopeAsync(scope, false, throwException)) { scope.SetEnabled(true); SaveScopeFile(scope); return true; } } return false; } public bool DisableScope(string name, bool throwException = false) { if (_scopes.TryGetValue(name, out Scope scope)) { if (scope.Enabled && DeactivateScope(scope, throwException)) { scope.SetEnabled(false); SaveScopeFile(scope); return true; } } return false; } public void SaveScope(string name) { if (_scopes.TryGetValue(name, out Scope scope)) SaveScopeFile(scope); } public IDictionary GetAddressHostNameMap() { Dictionary map = new Dictionary(); foreach (KeyValuePair scope in _scopes) { foreach (KeyValuePair lease in scope.Value.Leases) { if (!string.IsNullOrEmpty(lease.Value.HostName)) map.Add(lease.Value.Address.ToString(), lease.Value.HostName); } } return map; } #endregion #region properties public IReadOnlyDictionary Scopes { get { return _scopes; } } public DnsServer DnsServer { get { return _dnsServer; } set { _dnsServer = value; } } internal AuthManager AuthManager { get { return _authManager; } set { _authManager = value; } } #endregion class UdpListener { #region private readonly Socket _socket; volatile int _scopeCount; #endregion #region constructor public UdpListener(Socket socket) { _socket = socket; } #endregion #region public public void IncrementScopeCount() { Interlocked.Increment(ref _scopeCount); } public void DecrementScopeCount() { Interlocked.Decrement(ref _scopeCount); } #endregion #region properties public Socket Socket { get { return _socket; } } public int ScopeCount { get { return _scopeCount; } } #endregion } } } ================================================ FILE: DnsServerCore/Dhcp/DhcpServerException.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; namespace DnsServerCore.Dhcp { public class DhcpServerException : Exception { #region constructors public DhcpServerException() : base() { } public DhcpServerException(string message) : base(message) { } public DhcpServerException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Exclusion.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 System; using System.Net; using TechnitiumLibrary.Net; namespace DnsServerCore.Dhcp { public class Exclusion { #region variables readonly IPAddress _startingAddress; readonly IPAddress _endingAddress; #endregion #region constructor public Exclusion(IPAddress startingAddress, IPAddress endingAddress) { if (startingAddress.ConvertIpToNumber() > endingAddress.ConvertIpToNumber()) throw new ArgumentException("Exclusion ending address must be greater than or equal to starting address."); _startingAddress = startingAddress; _endingAddress = endingAddress; } #endregion #region properties public IPAddress StartingAddress { get { return _startingAddress; } } public IPAddress EndingAddress { get { return _endingAddress; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Lease.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.Dhcp.Options; using System; using System.Globalization; using System.IO; using System.Net; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; namespace DnsServerCore.Dhcp { public enum LeaseType : byte { None = 0, Dynamic = 1, Reserved = 2 } public class Lease : IComparable { #region variables static readonly char[] _hyphenColonSeparator = new char[] { '-', ':' }; LeaseType _type; readonly ClientIdentifierOption _clientIdentifier; string _hostName; readonly byte[] _hardwareAddress; readonly IPAddress _address; string _comments; readonly DateTime _leaseObtained; DateTime _leaseExpires; #endregion #region constructor internal Lease(LeaseType type, ClientIdentifierOption clientIdentifier, string hostName, byte[] hardwareAddress, IPAddress address, string comments, uint leaseTime) { _type = type; _clientIdentifier = clientIdentifier; _hostName = hostName; _hardwareAddress = hardwareAddress; _address = address; _comments = comments; _leaseObtained = DateTime.UtcNow; ExtendLease(leaseTime); } internal Lease(LeaseType type, string hostName, DhcpMessageHardwareAddressType hardwareAddressType, byte[] hardwareAddress, IPAddress address, string comments) : this(type, new ClientIdentifierOption((byte)hardwareAddressType, hardwareAddress), hostName, hardwareAddress, address, comments, 0) { } internal Lease(LeaseType type, string hostName, DhcpMessageHardwareAddressType hardwareAddressType, string hardwareAddress, IPAddress address, string comments) : this(type, hostName, hardwareAddressType, ParseHardwareAddress(hardwareAddress), address, comments) { } internal Lease(BinaryReader bR) { byte version = bR.ReadByte(); switch (version) { case 1: case 2: _type = (LeaseType)bR.ReadByte(); _clientIdentifier = DhcpOption.Parse(bR.BaseStream) as ClientIdentifierOption; _clientIdentifier.ParseOptionValue(); _hostName = bR.ReadShortString(); if (string.IsNullOrWhiteSpace(_hostName)) _hostName = null; _hardwareAddress = bR.ReadBuffer(); _address = IPAddressExtensions.ReadFrom(bR); if (version >= 2) { _comments = bR.ReadShortString(); if (string.IsNullOrWhiteSpace(_comments)) _comments = null; } _leaseObtained = bR.ReadDateTime(); _leaseExpires = bR.ReadDateTime(); break; default: throw new InvalidDataException("Lease data format version not supported."); } } #endregion #region internal internal static byte[] ParseHardwareAddress(string hardwareAddress) { string[] parts = hardwareAddress.Split(_hyphenColonSeparator); byte[] address = new byte[parts.Length]; for (int i = 0; i < parts.Length; i++) address[i] = byte.Parse(parts[i], NumberStyles.HexNumber, CultureInfo.InvariantCulture); return address; } internal void ConvertToReserved() { _type = LeaseType.Reserved; } internal void ConvertToDynamic() { _type = LeaseType.Dynamic; } internal void SetHostName(string hostName) { _hostName = hostName; } #endregion #region public public void ExtendLease(uint leaseTime) { _leaseExpires = DateTime.UtcNow.AddSeconds(leaseTime); } public void WriteTo(BinaryWriter bW) { bW.Write((byte)2); //version bW.Write((byte)_type); _clientIdentifier.WriteTo(bW.BaseStream); if (string.IsNullOrWhiteSpace(_hostName)) bW.Write((byte)0); else bW.WriteShortString(_hostName); bW.WriteBuffer(_hardwareAddress); _address.WriteTo(bW); if (string.IsNullOrWhiteSpace(_comments)) bW.Write((byte)0); else bW.WriteShortString(_comments); bW.Write(_leaseObtained); bW.Write(_leaseExpires); } public string GetClientInfo() { string hardwareAddress = BitConverter.ToString(_hardwareAddress); if (string.IsNullOrWhiteSpace(_hostName)) return "[" + hardwareAddress + "]"; return _hostName + " [" + hardwareAddress + "]"; } public int CompareTo(Lease other) { return _address.ConvertIpToNumber().CompareTo(other._address.ConvertIpToNumber()); } #endregion #region properties public LeaseType Type { get { return _type; } } internal ClientIdentifierOption ClientIdentifier { get { return _clientIdentifier; } } public string HostName { get { return _hostName; } } public byte[] HardwareAddress { get { return _hardwareAddress; } } public IPAddress Address { get { return _address; } } public string Comments { get { return _comments; } set { _comments = value; } } public DateTime LeaseObtained { get { return _leaseObtained; } } public DateTime LeaseExpires { get { return _leaseExpires; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/BroadcastAddressOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class BroadcastAddressOption : DhcpOption { #region variables IPAddress _broadcastAddress; #endregion #region constructor public BroadcastAddressOption(IPAddress broadcastAddress) : base(DhcpOptionCode.BroadcastAddress) { _broadcastAddress = broadcastAddress; } public BroadcastAddressOption(Stream s) : base(DhcpOptionCode.BroadcastAddress, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); _broadcastAddress = new IPAddress(s.ReadExactly(4)); } protected override void WriteOptionValue(Stream s) { s.Write(_broadcastAddress.GetAddressBytes()); } #endregion #region properties public IPAddress BroadcastAddress { get { return _broadcastAddress; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/CAPWAPAccessControllerOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class CAPWAPAccessControllerOption : DhcpOption { #region variables IReadOnlyCollection _apIpAddresses; #endregion #region constructor public CAPWAPAccessControllerOption(IReadOnlyCollection apIpAddresses) : base(DhcpOptionCode.CAPWAPAccessControllerAddresses) { _apIpAddresses = apIpAddresses; } public CAPWAPAccessControllerOption(Stream s) : base(DhcpOptionCode.CAPWAPAccessControllerAddresses, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 1) throw new InvalidDataException(); List apIpAddresses = new List(); while (s.Length > 0) apIpAddresses.Add(new IPAddress(s.ReadExactly(4))); _apIpAddresses = apIpAddresses; } protected override void WriteOptionValue(Stream s) { foreach (IPAddress apIpAddress in _apIpAddresses) s.Write(apIpAddress.GetAddressBytes()); } #endregion #region properties public IReadOnlyCollection ApIpAddresses { get { return _apIpAddresses; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/ClasslessStaticRouteOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; namespace DnsServerCore.Dhcp.Options { public class ClasslessStaticRouteOption : DhcpOption { #region variables IReadOnlyCollection _routes; #endregion #region constructor public ClasslessStaticRouteOption(IReadOnlyCollection routes) : base(DhcpOptionCode.ClasslessStaticRoute) { _routes = routes; } public ClasslessStaticRouteOption(Stream s) : base(DhcpOptionCode.ClasslessStaticRoute, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 5) throw new InvalidDataException(); List routes = new List(); while (s.Position < s.Length) { routes.Add(new Route(s)); } _routes = routes; } protected override void WriteOptionValue(Stream s) { foreach (Route route in _routes) route.WriteTo(s); } #endregion #region properties public IReadOnlyCollection Routes { get { return _routes; } } #endregion public class Route { #region private readonly IPAddress _destination; readonly IPAddress _subnetMask; readonly IPAddress _router; #endregion #region constructor public Route(IPAddress destination, IPAddress subnetMask, IPAddress router) { _destination = destination; _subnetMask = subnetMask; _router = router; } public Route(Stream s) { int subnetMaskWidth = s.ReadByte(); if (subnetMaskWidth < 0) throw new EndOfStreamException(); byte[] destinationBuffer = new byte[4]; s.ReadExactly(destinationBuffer, 0, Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8))); _destination = new IPAddress(destinationBuffer); _subnetMask = IPAddressExtensions.GetSubnetMask(subnetMaskWidth); _router = new IPAddress(s.ReadExactly(4)); } #endregion #region public public void WriteTo(Stream s) { byte subnetMaskWidth = (byte)_subnetMask.GetSubnetMaskWidth(); s.WriteByte(subnetMaskWidth); s.Write(_destination.GetAddressBytes(), 0, Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8))); s.Write(_router.GetAddressBytes()); } #endregion #region properties public IPAddress Destination { get { return _destination; } } public IPAddress SubnetMask { get { return _subnetMask; } } public IPAddress Router { get { return _router; } } #endregion } } } ================================================ FILE: DnsServerCore/Dhcp/Options/ClientFullyQualifiedDomainNameOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using System.Text; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.Dhcp.Options { [Flags] enum ClientFullyQualifiedDomainNameFlags : byte { None = 0, ShouldUpdateDns = 1, OverrideByServer = 2, EncodeUsingCanonicalWireFormat = 4, NoDnsUpdate = 8, } class ClientFullyQualifiedDomainNameOption : DhcpOption { #region variables ClientFullyQualifiedDomainNameFlags _flags; byte _rcode1; byte _rcode2; string _domainName; #endregion #region constructor public ClientFullyQualifiedDomainNameOption(ClientFullyQualifiedDomainNameFlags flags, byte rcode1, byte rcode2, string domainName) : base(DhcpOptionCode.ClientFullyQualifiedDomainName) { _flags = flags; _rcode1 = rcode1; _rcode2 = rcode2; _domainName = domainName; } public ClientFullyQualifiedDomainNameOption(Stream s) : base(DhcpOptionCode.ClientFullyQualifiedDomainName, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 3) throw new InvalidDataException(); int flags = s.ReadByte(); if (flags < 0) throw new EndOfStreamException(); _flags = (ClientFullyQualifiedDomainNameFlags)flags; int rcode; rcode = s.ReadByte(); if (rcode < 0) throw new EndOfStreamException(); _rcode1 = (byte)rcode; rcode = s.ReadByte(); if (rcode < 0) throw new EndOfStreamException(); _rcode2 = (byte)rcode; if (_flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat)) _domainName = DnsDatagram.DeserializeDomainName(s, 0, true); else _domainName = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length - 3)); } protected override void WriteOptionValue(Stream s) { s.WriteByte((byte)_flags); s.WriteByte(_rcode1); s.WriteByte(_rcode2); if (_flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat)) DnsDatagram.SerializeDomainName(_domainName, s); else s.Write(Encoding.ASCII.GetBytes(_domainName)); } #endregion #region properties public ClientFullyQualifiedDomainNameFlags Flags { get { return _flags; } } public byte RCODE1 { get { return _rcode1; } } public byte RCODE2 { get { return _rcode2; } } public string DomainName { get { return _domainName; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/ClientIdentifierOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { public class ClientIdentifierOption : DhcpOption, IEquatable { #region variables byte _type; byte[] _identifier; #endregion #region constructor public ClientIdentifierOption(byte type, byte[] identifier) : base(DhcpOptionCode.ClientIdentifier) { _type = type; _identifier = identifier; } public ClientIdentifierOption(Stream s) : base(DhcpOptionCode.ClientIdentifier, s) { } #endregion #region static public static ClientIdentifierOption Parse(string clientIdentifier) { string[] parts = clientIdentifier.Split('-'); return new ClientIdentifierOption(byte.Parse(parts[0]), Convert.FromHexString(parts[1])); } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 2) throw new InvalidDataException(); int type = s.ReadByte(); if (type < 0) throw new EndOfStreamException(); _type = (byte)type; _identifier = s.ReadExactly((int)s.Length - 1); } protected override void WriteOptionValue(Stream s) { s.WriteByte(_type); s.Write(_identifier); } #endregion #region public public override bool Equals(object obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; return Equals(obj as ClientIdentifierOption); } public bool Equals(ClientIdentifierOption other) { if (other is null) return false; if (this._type != other._type) return false; if (this._identifier.Length != other._identifier.Length) return false; for (int i = 0; i < this._identifier.Length; i++) { if (this._identifier[i] != other._identifier[i]) return false; } return true; } public override int GetHashCode() { return HashCode.Combine(_type, _identifier.GetArrayHashCode()); } public override string ToString() { return _type.ToString() + "-" + Convert.ToHexString(_identifier); } #endregion #region properties public byte Type { get { return _type; } } public byte[] Identifier { get { return _identifier; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/DhcpMessageTypeOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2019 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 System.IO; namespace DnsServerCore.Dhcp.Options { enum DhcpMessageType : byte { Unknown = 0, Discover = 1, Offer = 2, Request = 3, Decline = 4, Ack = 5, Nak = 6, Release = 7, Inform = 8 } class DhcpMessageTypeOption : DhcpOption { #region variables DhcpMessageType _type; #endregion #region constructor public DhcpMessageTypeOption(DhcpMessageType type) : base(DhcpOptionCode.DhcpMessageType) { _type = type; } public DhcpMessageTypeOption(Stream s) : base(DhcpOptionCode.DhcpMessageType, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 1) throw new InvalidDataException(); int type = s.ReadByte(); if (type < 0) throw new EndOfStreamException(); _type = (DhcpMessageType)type; } protected override void WriteOptionValue(Stream s) { s.WriteByte((byte)_type); } #endregion #region string public override string ToString() { return _type.ToString(); } #endregion #region properties public DhcpMessageType Type { get { return _type; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/DomainNameOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Text; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class DomainNameOption : DhcpOption { #region variables string _domainName; #endregion #region constructor public DomainNameOption(string domainName) : base(DhcpOptionCode.DomainName) { _domainName = domainName; } public DomainNameOption(Stream s) : base(DhcpOptionCode.DomainName, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 1) throw new InvalidDataException(); _domainName = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length)); } protected override void WriteOptionValue(Stream s) { s.Write(Encoding.ASCII.GetBytes(_domainName)); } #endregion #region properties public string DomainName { get { return _domainName; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/DomainNameServerOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class DomainNameServerOption : DhcpOption { #region variables IReadOnlyCollection _addresses; #endregion #region constructor public DomainNameServerOption(IReadOnlyCollection addresses) : base(DhcpOptionCode.DomainNameServer) { _addresses = addresses; } public DomainNameServerOption(Stream s) : base(DhcpOptionCode.DomainNameServer, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if ((s.Length % 4 != 0) || (s.Length < 4)) throw new InvalidDataException(); IPAddress[] addresses = new IPAddress[s.Length / 4]; for (int i = 0; i < addresses.Length; i++) addresses[i] = new IPAddress(s.ReadExactly(4)); _addresses = addresses; } protected override void WriteOptionValue(Stream s) { foreach (IPAddress address in _addresses) s.Write(address.GetAddressBytes()); } #endregion #region properties public IReadOnlyCollection Addresses { get { return _addresses; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/DomainSearchOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2022 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 System.Collections.Generic; using System.IO; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.Dhcp.Options { class DomainSearchOption : DhcpOption { #region variables IReadOnlyCollection _searchStrings; #endregion #region constructor public DomainSearchOption(IReadOnlyCollection searchStrings) : base(DhcpOptionCode.DomainSearch) { _searchStrings = searchStrings; } public DomainSearchOption(Stream s) : base(DhcpOptionCode.DomainSearch, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 1) throw new InvalidDataException(); List searchStrings = new List(); while (s.Length > 0) searchStrings.Add(DnsDatagram.DeserializeDomainName(s)); _searchStrings = searchStrings; } protected override void WriteOptionValue(Stream s) { List domainEntries = new List(1); foreach (string searchString in _searchStrings) DnsDatagram.SerializeDomainName(searchString, s, domainEntries); } #endregion #region properties public IReadOnlyCollection SearchStrings { get { return _searchStrings; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/HostNameOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Text; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class HostNameOption : DhcpOption { #region variables string _hostName; #endregion #region constructor public HostNameOption(string hostName) : base(DhcpOptionCode.HostName) { _hostName = hostName; } public HostNameOption(Stream s) : base(DhcpOptionCode.HostName, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 1) throw new InvalidDataException(); _hostName = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length)); } protected override void WriteOptionValue(Stream s) { s.Write(Encoding.ASCII.GetBytes(_hostName)); } #endregion #region properties public string HostName { get { return _hostName; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/IpAddressLeaseTimeOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class IpAddressLeaseTimeOption : DhcpOption { #region variables uint _leaseTime; #endregion #region constructor public IpAddressLeaseTimeOption(uint leaseTime) : base(DhcpOptionCode.IpAddressLeaseTime) { _leaseTime = leaseTime; } public IpAddressLeaseTimeOption(Stream s) : base(DhcpOptionCode.IpAddressLeaseTime, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); byte[] buffer = s.ReadExactly(4); Array.Reverse(buffer); _leaseTime = BitConverter.ToUInt32(buffer, 0); } protected override void WriteOptionValue(Stream s) { byte[] buffer = BitConverter.GetBytes(_leaseTime); Array.Reverse(buffer); s.Write(buffer); } #endregion #region properties public uint LeaseTime { get { return _leaseTime; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/MaximumDhcpMessageSizeOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class MaximumDhcpMessageSizeOption : DhcpOption { #region variables ushort _length; #endregion #region constructor public MaximumDhcpMessageSizeOption(ushort length) : base(DhcpOptionCode.MaximumDhcpMessageSize) { if (length < 576) throw new ArgumentOutOfRangeException(nameof(length), "Length must be 576 bytes or more."); _length = length; } public MaximumDhcpMessageSizeOption(Stream s) : base(DhcpOptionCode.MaximumDhcpMessageSize, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 2) throw new InvalidDataException(); byte[] buffer = s.ReadExactly(2); Array.Reverse(buffer); _length = BitConverter.ToUInt16(buffer, 0); if (_length < 576) _length = 576; } protected override void WriteOptionValue(Stream s) { byte[] buffer = BitConverter.GetBytes(_length); Array.Reverse(buffer); s.Write(buffer); } #endregion #region properties public uint Length { get { return _length; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/NetBiosNameServerOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class NetBiosNameServerOption : DhcpOption { #region variables IReadOnlyCollection _addresses; #endregion #region constructor public NetBiosNameServerOption(IReadOnlyCollection addresses) : base(DhcpOptionCode.NetBiosOverTcpIpNameServer) { _addresses = addresses; } public NetBiosNameServerOption(Stream s) : base(DhcpOptionCode.NetBiosOverTcpIpNameServer, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if ((s.Length % 4 != 0) || (s.Length < 4)) throw new InvalidDataException(); IPAddress[] addresses = new IPAddress[s.Length / 4]; for (int i = 0; i < addresses.Length; i++) addresses[i] = new IPAddress(s.ReadExactly(4)); _addresses = addresses; } protected override void WriteOptionValue(Stream s) { foreach (IPAddress address in _addresses) s.Write(address.GetAddressBytes()); } #endregion #region properties public IReadOnlyCollection Addresses { get { return _addresses; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/NetworkTimeProtocolServersOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class NetworkTimeProtocolServersOption : DhcpOption { #region variables IReadOnlyCollection _addresses; #endregion #region constructor public NetworkTimeProtocolServersOption(IReadOnlyCollection addresses) : base(DhcpOptionCode.NetworkTimeProtocolServers) { _addresses = addresses; } public NetworkTimeProtocolServersOption(Stream s) : base(DhcpOptionCode.NetworkTimeProtocolServers, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if ((s.Length % 4 != 0) || (s.Length < 4)) throw new InvalidDataException(); IPAddress[] addresses = new IPAddress[s.Length / 4]; for (int i = 0; i < addresses.Length; i++) addresses[i] = new IPAddress(s.ReadExactly(4)); _addresses = addresses; } protected override void WriteOptionValue(Stream s) { foreach (IPAddress address in _addresses) s.Write(address.GetAddressBytes()); } #endregion #region properties public IReadOnlyCollection Addresses { get { return _addresses; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/OptionOverloadOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 System; using System.IO; namespace DnsServerCore.Dhcp.Options { [Flags] enum OptionOverloadValue : byte { FileFieldUsed = 1, SnameFieldUsed = 2, BothFieldsUsed = 3 } class OptionOverloadOption : DhcpOption { #region variables OptionOverloadValue _value; #endregion #region constructor public OptionOverloadOption(OptionOverloadValue value) : base(DhcpOptionCode.OptionOverload) { _value = value; } public OptionOverloadOption(Stream s) : base(DhcpOptionCode.OptionOverload, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 1) throw new InvalidDataException(); int value = s.ReadByte(); if (value < 0) throw new EndOfStreamException(); _value = (OptionOverloadValue)value; } protected override void WriteOptionValue(Stream s) { s.WriteByte((byte)_value); } #endregion #region properties public OptionOverloadValue Value { get { return _value; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/ParameterRequestListOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2019 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 System.IO; namespace DnsServerCore.Dhcp.Options { class ParameterRequestListOption : DhcpOption { #region variables DhcpOptionCode[] _optionCodes; #endregion #region constructor public ParameterRequestListOption(DhcpOptionCode[] optionCodes) : base(DhcpOptionCode.ParameterRequestList) { _optionCodes = optionCodes; } public ParameterRequestListOption(Stream s) : base(DhcpOptionCode.ParameterRequestList, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length < 1) throw new InvalidDataException(); _optionCodes = new DhcpOptionCode[s.Length]; int optionCode; for (int i = 0; i < _optionCodes.Length; i++) { optionCode = s.ReadByte(); if (optionCode < 0) throw new EndOfStreamException(); _optionCodes[i] = (DhcpOptionCode)optionCode; } } protected override void WriteOptionValue(Stream s) { foreach (DhcpOptionCode optionCode in _optionCodes) s.WriteByte((byte)optionCode); } #endregion #region properties public DhcpOptionCode[] OptionCodes { get { return _optionCodes; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/RebindingTimeValueOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class RebindingTimeValueOption : DhcpOption { #region variables uint _t2Interval; #endregion #region constructor public RebindingTimeValueOption(uint t2Interval) : base(DhcpOptionCode.RebindingTimeValue) { _t2Interval = t2Interval; } public RebindingTimeValueOption(Stream s) : base(DhcpOptionCode.RebindingTimeValue, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); byte[] buffer = s.ReadExactly(4); Array.Reverse(buffer); _t2Interval = BitConverter.ToUInt32(buffer, 0); } protected override void WriteOptionValue(Stream s) { byte[] buffer = BitConverter.GetBytes(_t2Interval); Array.Reverse(buffer); s.Write(buffer); } #endregion #region properties public uint T2Interval { get { return _t2Interval; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/RenewalTimeValueOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class RenewalTimeValueOption : DhcpOption { #region variables uint _t1Interval; #endregion #region constructor public RenewalTimeValueOption(uint t1Interval) : base(DhcpOptionCode.RenewalTimeValue) { _t1Interval = t1Interval; } public RenewalTimeValueOption(Stream s) : base(DhcpOptionCode.RenewalTimeValue, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); byte[] buffer = s.ReadExactly(4); Array.Reverse(buffer); _t1Interval = BitConverter.ToUInt32(buffer, 0); } protected override void WriteOptionValue(Stream s) { byte[] buffer = BitConverter.GetBytes(_t1Interval); Array.Reverse(buffer); s.Write(buffer); } #endregion #region properties public uint T1Interval { get { return _t1Interval; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/RequestedIpAddressOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class RequestedIpAddressOption : DhcpOption { #region variables IPAddress _address; #endregion #region constructor public RequestedIpAddressOption(IPAddress address) : base(DhcpOptionCode.RequestedIpAddress) { _address = address; } public RequestedIpAddressOption(Stream s) : base(DhcpOptionCode.RequestedIpAddress, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); _address = new IPAddress(s.ReadExactly(4)); } protected override void WriteOptionValue(Stream s) { s.Write(_address.GetAddressBytes()); } #endregion #region properties public IPAddress Address { get { return _address; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/RouterOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class RouterOption : DhcpOption { #region variables IPAddress[] _addresses; #endregion #region constructor public RouterOption(IPAddress[] addresses) : base(DhcpOptionCode.Router) { _addresses = addresses; } public RouterOption(Stream s) : base(DhcpOptionCode.Router, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if ((s.Length % 4 != 0) || (s.Length < 4)) throw new InvalidDataException(); _addresses = new IPAddress[s.Length / 4]; for (int i = 0; i < _addresses.Length; i++) _addresses[i] = new IPAddress(s.ReadExactly(4)); } protected override void WriteOptionValue(Stream s) { foreach (IPAddress address in _addresses) s.Write(address.GetAddressBytes()); } #endregion #region properties public IPAddress[] Addresses { get { return _addresses; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/ServerIdentifierOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class ServerIdentifierOption : DhcpOption { #region variables IPAddress _address; #endregion #region constructor public ServerIdentifierOption(IPAddress address) : base(DhcpOptionCode.ServerIdentifier) { _address = address; } public ServerIdentifierOption(Stream s) : base(DhcpOptionCode.ServerIdentifier, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); _address = new IPAddress(s.ReadExactly(4)); } protected override void WriteOptionValue(Stream s) { s.Write(_address.GetAddressBytes()); } #endregion #region properties public IPAddress Address { get { return _address; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/SubnetMaskOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class SubnetMaskOption : DhcpOption { #region variables IPAddress _subnetMask; #endregion #region constructor public SubnetMaskOption(IPAddress subnetMask) : base(DhcpOptionCode.SubnetMask) { _subnetMask = subnetMask; } public SubnetMaskOption(Stream s) : base(DhcpOptionCode.SubnetMask, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if (s.Length != 4) throw new InvalidDataException(); _subnetMask = new IPAddress(s.ReadExactly(4)); } protected override void WriteOptionValue(Stream s) { s.Write(_subnetMask.GetAddressBytes()); } #endregion #region properties public IPAddress SubnetMask { get { return _subnetMask; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/TftpServerAddressOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using System.IO; using System.Net; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class TftpServerAddressOption : DhcpOption { #region variables IReadOnlyCollection _addresses; #endregion #region constructor public TftpServerAddressOption(IReadOnlyCollection addresses) : base(DhcpOptionCode.TftpServerAddress) { _addresses = addresses; } public TftpServerAddressOption(Stream s) : base(DhcpOptionCode.TftpServerAddress, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { if ((s.Length % 4 != 0) || (s.Length < 4)) throw new InvalidDataException(); IPAddress[] addresses = new IPAddress[s.Length / 4]; for (int i = 0; i < addresses.Length; i++) addresses[i] = new IPAddress(s.ReadExactly(4)); _addresses = addresses; } protected override void WriteOptionValue(Stream s) { foreach (IPAddress address in _addresses) s.Write(address.GetAddressBytes()); } #endregion #region properties public IReadOnlyCollection Addresses { get { return _addresses; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/VendorClassIdentifierOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; using System.Text; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { class VendorClassIdentifierOption : DhcpOption { #region variables string _identifier; #endregion #region constructor public VendorClassIdentifierOption(string identifier) : base(DhcpOptionCode.VendorClassIdentifier) { _identifier = identifier; } public VendorClassIdentifierOption(Stream s) : base(DhcpOptionCode.VendorClassIdentifier, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { _identifier = Encoding.ASCII.GetString(s.ReadExactly((int)s.Length)); } protected override void WriteOptionValue(Stream s) { s.Write(Encoding.ASCII.GetBytes(_identifier)); } #endregion #region properties public string Identifier { get { return _identifier; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Options/VendorSpecificInformationOption.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary; using TechnitiumLibrary.IO; namespace DnsServerCore.Dhcp.Options { public class VendorSpecificInformationOption : DhcpOption { #region variables byte[] _information; #endregion #region constructor public VendorSpecificInformationOption(string hexInfo) : base(DhcpOptionCode.VendorSpecificInformation) { if (hexInfo.Contains(':')) _information = hexInfo.ParseColonHexString(); else _information = Convert.FromHexString(hexInfo); } public VendorSpecificInformationOption(byte[] information) : base(DhcpOptionCode.VendorSpecificInformation) { _information = information; } public VendorSpecificInformationOption(Stream s) : base(DhcpOptionCode.VendorSpecificInformation, s) { } #endregion #region protected protected override void ParseOptionValue(Stream s) { _information = s.ReadExactly((int)s.Length); } protected override void WriteOptionValue(Stream s) { s.Write(_information); } #endregion #region properties public byte[] Information { get { return _information; } } #endregion } } ================================================ FILE: DnsServerCore/Dhcp/Scope.cs ================================================ /* 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.Dhcp.Options; using DnsServerCore.Dns; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dhcp { public sealed class Scope : IComparable, IDisposable { #region variables //required parameters string _name; bool _enabled; IPAddress _startingAddress; IPAddress _endingAddress; IPAddress _subnetMask; ushort _leaseTimeDays = 1; //default 1 day lease byte _leaseTimeHours = 0; byte _leaseTimeMinutes = 0; ushort _offerDelayTime; readonly LogManager _log; readonly DhcpServer _dhcpServer; bool _pingCheckEnabled; ushort _pingCheckTimeout = 1000; byte _pingCheckRetries = 2; //dhcp options string _domainName; IReadOnlyCollection _domainSearchList; bool _dnsUpdates = true; bool _dnsOverwriteForDynamicLease = false; uint _dnsTtl = 900; IPAddress _serverAddress; string _serverHostName; string _bootFileName; IPAddress _routerAddress; bool _useThisDnsServer; IReadOnlyCollection _dnsServers; IReadOnlyCollection _winsServers; IReadOnlyCollection _ntpServers; IReadOnlyCollection _ntpServerDomainNames; IReadOnlyCollection _staticRoutes; IReadOnlyDictionary _vendorInfo; IReadOnlyCollection _capwapAcIpAddresses; IReadOnlyCollection _tftpServerAddreses; //advanced options IReadOnlyCollection _genericOptions; IReadOnlyCollection _exclusions; readonly ConcurrentDictionary _reservedLeases = new ConcurrentDictionary(); bool _allowOnlyReservedLeases; bool _blockLocallyAdministeredMacAddresses; bool _ignoreClientIdentifierOption; //leases readonly ConcurrentDictionary _leases = new ConcurrentDictionary(); //internal computed parameters IPAddress _networkAddress; IPAddress _broadcastAddress; //internal parameters const int OFFER_EXPIRY_SECONDS = 60; //1 mins offer expiry readonly ConcurrentDictionary _offers = new ConcurrentDictionary(); IPAddress _lastAddressOffered; readonly SemaphoreSlim _lastAddressOfferedLock = new SemaphoreSlim(1, 1); IPAddress _interfaceAddress; int _interfaceIndex; DateTime _lastModified = DateTime.UtcNow; #endregion #region constructor public Scope(string name, bool enabled, IPAddress startingAddress, IPAddress endingAddress, IPAddress subnetMask, LogManager log, DhcpServer dhcpServer) { ValidateScopeName(name); _name = name; _enabled = enabled; ChangeNetwork(startingAddress, endingAddress, subnetMask); _log = log; _dhcpServer = dhcpServer; } public Scope(Stream s, LogManager log, DhcpServer dhcpServer) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "SC") throw new InvalidDataException("DhcpServer scope file format is invalid."); _log = log; _dhcpServer = dhcpServer; byte version = bR.ReadByte(); switch (version) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: _name = bR.ReadShortString(); _enabled = bR.ReadBoolean(); ChangeNetwork(IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR)); _leaseTimeDays = bR.ReadUInt16(); _leaseTimeHours = bR.ReadByte(); _leaseTimeMinutes = bR.ReadByte(); _offerDelayTime = bR.ReadUInt16(); if (version >= 5) { _pingCheckEnabled = bR.ReadBoolean(); _pingCheckTimeout = bR.ReadUInt16(); _pingCheckRetries = bR.ReadByte(); } _domainName = bR.ReadShortString(); if (string.IsNullOrWhiteSpace(_domainName)) _domainName = null; if (version >= 7) { int count = bR.ReadByte(); if (count > 0) { string[] domainSearchStrings = new string[count]; for (int i = 0; i < count; i++) domainSearchStrings[i] = bR.ReadShortString(); _domainSearchList = domainSearchStrings; } _dnsUpdates = bR.ReadBoolean(); } if (version >= 10) _dnsOverwriteForDynamicLease = bR.ReadBoolean(); _dnsTtl = bR.ReadUInt32(); if (version >= 2) { _serverAddress = IPAddressExtensions.ReadFrom(bR); if (_serverAddress.Equals(IPAddress.Any)) _serverAddress = null; } if (version >= 3) { _serverHostName = bR.ReadShortString(); if (string.IsNullOrEmpty(_serverHostName)) _serverHostName = null; _bootFileName = bR.ReadShortString(); if (string.IsNullOrEmpty(_bootFileName)) _bootFileName = null; } _routerAddress = IPAddressExtensions.ReadFrom(bR); if (_routerAddress.Equals(IPAddress.Any)) _routerAddress = null; { int count = bR.ReadByte(); if (count > 0) { if (count == 255) { _useThisDnsServer = true; FindThisDnsServerAddress(); } else { IPAddress[] dnsServers = new IPAddress[count]; for (int i = 0; i < count; i++) dnsServers[i] = IPAddressExtensions.ReadFrom(bR); _dnsServers = dnsServers; } } } { int count = bR.ReadByte(); if (count > 0) { IPAddress[] winsServers = new IPAddress[count]; for (int i = 0; i < count; i++) winsServers[i] = IPAddressExtensions.ReadFrom(bR); _winsServers = winsServers; } } { int count = bR.ReadByte(); if (count > 0) { IPAddress[] ntpServers = new IPAddress[count]; for (int i = 0; i < count; i++) ntpServers[i] = IPAddressExtensions.ReadFrom(bR); _ntpServers = ntpServers; } } if (version >= 7) { int count = bR.ReadByte(); if (count > 0) { string[] ntpServerDomainNames = new string[count]; for (int i = 0; i < count; i++) ntpServerDomainNames[i] = bR.ReadShortString(); _ntpServerDomainNames = ntpServerDomainNames; } } { int count = bR.ReadByte(); if (count > 0) { ClasslessStaticRouteOption.Route[] staticRoutes = new ClasslessStaticRouteOption.Route[count]; for (int i = 0; i < count; i++) staticRoutes[i] = new ClasslessStaticRouteOption.Route(bR.BaseStream); _staticRoutes = staticRoutes; } } if (version >= 4) { int count = bR.ReadByte(); if (count > 0) { Dictionary vendorInfo = new Dictionary(count); for (int i = 0; i < count; i++) { string vendorClassIdentifier = bR.ReadShortString(); VendorSpecificInformationOption vendorSpecificInformation = new VendorSpecificInformationOption(bR.ReadBuffer()); vendorInfo.Add(vendorClassIdentifier, vendorSpecificInformation); } _vendorInfo = vendorInfo; } } if (version >= 7) { int count = bR.ReadByte(); if (count > 0) { IPAddress[] capwapAcIpAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) capwapAcIpAddresses[i] = IPAddressExtensions.ReadFrom(bR); _capwapAcIpAddresses = capwapAcIpAddresses; } } if (version >= 8) { int count = bR.ReadByte(); if (count > 0) { IPAddress[] tftpServerAddreses = new IPAddress[count]; for (int i = 0; i < count; i++) tftpServerAddreses[i] = IPAddressExtensions.ReadFrom(bR); _tftpServerAddreses = tftpServerAddreses; } } if (version >= 8) { int count = bR.ReadByte(); if (count > 0) { DhcpOption[] genericOptions = new DhcpOption[count]; for (int i = 0; i < count; i++) { DhcpOptionCode code = (DhcpOptionCode)bR.ReadByte(); short length = bR.ReadInt16(); byte[] value = bR.ReadBytes(length); genericOptions[i] = new DhcpOption(code, value); } _genericOptions = genericOptions; } } { int count = bR.ReadByte(); if (count > 0) { Exclusion[] exclusions = new Exclusion[count]; for (int i = 0; i < count; i++) exclusions[i] = new Exclusion(IPAddressExtensions.ReadFrom(bR), IPAddressExtensions.ReadFrom(bR)); _exclusions = exclusions; } } { int count = bR.ReadInt32(); if (count > 0) { for (int i = 0; i < count; i++) { Lease reservedLease = new Lease(bR); _reservedLeases.TryAdd(reservedLease.ClientIdentifier, reservedLease); } } _allowOnlyReservedLeases = bR.ReadBoolean(); } if (version >= 6) _blockLocallyAdministeredMacAddresses = bR.ReadBoolean(); else _blockLocallyAdministeredMacAddresses = false; if (version >= 9) _ignoreClientIdentifierOption = bR.ReadBoolean(); else _ignoreClientIdentifierOption = false; { int count = bR.ReadInt32(); if (count > 0) { for (int i = 0; i < count; i++) { Lease lease = new Lease(bR); _leases.TryAdd(lease.ClientIdentifier, lease); } } } break; default: throw new InvalidDataException("Scope data format version not supported."); } } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _lastAddressOfferedLock?.Dispose(); _disposed = true; GC.SuppressFinalize(this); } #endregion #region static internal static void ValidateScopeName(string name) { foreach (char invalidChar in Path.GetInvalidFileNameChars()) { if (name.Contains(invalidChar)) throw new DhcpServerException("The scope name contains an invalid character: " + invalidChar); } } private static bool IsAddressInRange(IPAddress address, IPAddress startingAddress, IPAddress endingAddress) { uint addressNumber = address.ConvertIpToNumber(); uint startingAddressNumber = startingAddress.ConvertIpToNumber(); uint endingAddressNumber = endingAddress.ConvertIpToNumber(); return (startingAddressNumber <= addressNumber) && (addressNumber <= endingAddressNumber); } private static void ValidateIpv4(IReadOnlyCollection value, string paramName) { if (value is not null) { foreach (IPAddress ip in value) { if (ip.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("The address must be an IPv4 address: " + ip.ToString(), paramName); } } } private static void ValidateIpv4(IPAddress value, string paramName) { if ((value is not null) && (value.AddressFamily != AddressFamily.InterNetwork)) throw new ArgumentException("The address must be an IPv4 address: " + value.ToString(), paramName); } #endregion #region private private async Task IsAddressAvailableAsync(IPAddress address) { if (address.Equals(_routerAddress)) return AddressStatus.FALSE; if ((_dnsServers != null) && _dnsServers.Contains(address)) return AddressStatus.FALSE; if ((_winsServers != null) && _winsServers.Contains(address)) return AddressStatus.FALSE; if ((_ntpServers != null) && _ntpServers.Contains(address)) return AddressStatus.FALSE; if (_exclusions != null) { foreach (Exclusion exclusion in _exclusions) { if (IsAddressInRange(address, exclusion.StartingAddress, exclusion.EndingAddress)) return new AddressStatus(false, exclusion.EndingAddress); } } foreach (KeyValuePair reservedLease in _reservedLeases) { if (address.Equals(reservedLease.Value.Address)) return AddressStatus.FALSE; } foreach (KeyValuePair lease in _leases) { if (address.Equals(lease.Value.Address)) return AddressStatus.FALSE; } foreach (KeyValuePair offer in _offers) { if (address.Equals(offer.Value.Address)) return AddressStatus.FALSE; } if (_pingCheckEnabled) { try { using (Ping ping = new Ping()) { int retry = 0; do { PingReply reply = await ping.SendPingAsync(address, _pingCheckTimeout); if (reply.Status == IPStatus.Success) return AddressStatus.FALSE; //address is in use } while (++retry < _pingCheckRetries); } } catch { } } return AddressStatus.TRUE; } private bool IsAddressAlreadyAllocated(IPAddress address, ClientIdentifierOption clientIdentifier) { foreach (KeyValuePair lease in _leases) { if (address.Equals(lease.Value.Address)) return !lease.Key.Equals(clientIdentifier); } foreach (KeyValuePair offer in _offers) { if (address.Equals(offer.Value.Address)) return !offer.Key.Equals(clientIdentifier); } return false; } private ClientFullyQualifiedDomainNameOption GetClientFullyQualifiedDomainNameOption(DhcpMessage request, string reservedLeaseHostName) { ClientFullyQualifiedDomainNameFlags responseFlags = ClientFullyQualifiedDomainNameFlags.None; if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat)) responseFlags |= ClientFullyQualifiedDomainNameFlags.EncodeUsingCanonicalWireFormat; if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.NoDnsUpdate)) { responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns; responseFlags |= ClientFullyQualifiedDomainNameFlags.OverrideByServer; } else if (request.ClientFullyQualifiedDomainName.Flags.HasFlag(ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns)) { responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns; } else { responseFlags |= ClientFullyQualifiedDomainNameFlags.ShouldUpdateDns; responseFlags |= ClientFullyQualifiedDomainNameFlags.OverrideByServer; } string clientDomainName; if (!string.IsNullOrWhiteSpace(reservedLeaseHostName)) { //domain name override by server clientDomainName = DhcpServer.GetSanitizedHostName(reservedLeaseHostName) + "." + _domainName; } else if (string.IsNullOrWhiteSpace(request.ClientFullyQualifiedDomainName.DomainName)) { //client domain empty and expects server for a fqdn domain name if (request.HostName is null) return null; //server unable to decide a name for client clientDomainName = DhcpServer.GetSanitizedHostName(request.HostName.HostName) + "." + _domainName; } else if (request.ClientFullyQualifiedDomainName.DomainName.Contains('.')) { //client domain is fqdn if (request.ClientFullyQualifiedDomainName.DomainName.EndsWith("." + _domainName, StringComparison.OrdinalIgnoreCase)) { clientDomainName = request.ClientFullyQualifiedDomainName.DomainName; } else { string[] parts = request.ClientFullyQualifiedDomainName.DomainName.Split('.'); clientDomainName = parts[0] + "." + _domainName; } } else { //client domain is just hostname clientDomainName = request.ClientFullyQualifiedDomainName.DomainName + "." + _domainName; } return new ClientFullyQualifiedDomainNameOption(responseFlags, 255, 255, clientDomainName); } private void ConvertToReservedLease(Lease lease) { //convert dynamic to reserved lease lease.ConvertToReserved(); //add reserved lease Lease reservedLease = new Lease(LeaseType.Reserved, null, DhcpMessageHardwareAddressType.Ethernet, lease.HardwareAddress, lease.Address, null); _reservedLeases[reservedLease.ClientIdentifier] = reservedLease; } private void ConvertToDynamicLease(Lease lease) { //convert reserved to dynamic lease lease.ConvertToDynamic(); //remove reserved lease Lease reservedLease = new Lease(LeaseType.Reserved, null, DhcpMessageHardwareAddressType.Ethernet, lease.HardwareAddress, lease.Address, null); _reservedLeases.TryRemove(reservedLease.ClientIdentifier, out _); //remove any old single address exclusion entry if (_exclusions != null) { foreach (Exclusion exclusion in _exclusions) { if (exclusion.StartingAddress.Equals(lease.Address) && exclusion.EndingAddress.Equals(lease.Address)) { //remove single address exclusion entry if (_exclusions.Count == 1) { _exclusions = null; } else { List exclusions = new List(); foreach (Exclusion exc in _exclusions) { if (exc.Equals(exclusion)) continue; exclusions.Add(exc); } _exclusions = exclusions; } break; } } } } #endregion #region internal internal bool FindInterface() { //find network with static ip address in scope range uint networkAddressNumber = _networkAddress.ConvertIpToNumber(); uint subnetMaskNumber = _subnetMask.ConvertIpToNumber(); foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties ipInterface = nic.GetIPProperties(); foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses) { if (ip.Address.AddressFamily == AddressFamily.InterNetwork) { uint addressNumber = ip.Address.ConvertIpToNumber(); if ((addressNumber & subnetMaskNumber) == networkAddressNumber) { //found interface for this scope range try { //check if interface has dynamic ipv4 address assigned via dhcp if (!OperatingSystem.IsMacOS()) { foreach (IPAddress dhcpServerAddress in ipInterface.DhcpServerAddresses) { if (dhcpServerAddress.AddressFamily == AddressFamily.InterNetwork) throw new DhcpServerException("DHCP Server requires static IP address to work correctly but the network interface was found to have a dynamic IP address [" + ip.Address.ToString() + "] assigned by another DHCP server: " + dhcpServerAddress.ToString()); } } } catch (PlatformNotSupportedException) { //DhcpServerAddresses() not supported on macOs //ignore the exception } _interfaceAddress = ip.Address; _interfaceIndex = ipInterface.GetIPv4Properties().Index; return true; } } } } try { if (!OperatingSystem.IsMacOS()) { //check if at least one interface has static ip address foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties ipInterface = nic.GetIPProperties(); foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses) { if (ip.Address.AddressFamily == AddressFamily.InterNetwork) { //check if address is static if (ipInterface.DhcpServerAddresses.Count < 1) { //found static ip address so this scope can be activated //using ANY ip address for this scope interface since we dont know the relay agent network _interfaceAddress = IPAddress.Any; _interfaceIndex = -1; return true; } } } } } } catch (PlatformNotSupportedException) { //DhcpServerAddresses() not supported on macOs //ignore the exception } //server has no static ip address configured return false; } internal void FindThisDnsServerAddress() { uint networkAddressNumber = _networkAddress.ConvertIpToNumber(); uint subnetMaskNumber = _subnetMask.ConvertIpToNumber(); DnsServer dnsServer = _dhcpServer.DnsServer; if (dnsServer is not null) { bool dnsOnAny = false; foreach (IPEndPoint localEP in dnsServer.LocalEndPoints) { if (localEP.Address.Equals(IPAddress.Any)) { dnsOnAny = true; break; } } if (!dnsOnAny) { //find local EP in scope network range foreach (IPEndPoint localEP in dnsServer.LocalEndPoints) { if (localEP.Address.AddressFamily == AddressFamily.InterNetwork) { uint addressNumber = localEP.Address.ConvertIpToNumber(); if ((addressNumber & subnetMaskNumber) == networkAddressNumber) { //found address in this scope range to use as dns server _dnsServers = new IPAddress[] { localEP.Address }; return; } } } //find any local EP available foreach (IPEndPoint localEP in dnsServer.LocalEndPoints) { if ((localEP.Address.AddressFamily == AddressFamily.InterNetwork) && !IPAddress.IsLoopback(localEP.Address)) { //found address to use as dns server _dnsServers = new IPAddress[] { localEP.Address }; return; } } //no useable address was found _dnsServers = null; return; } } NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); //find interface in current scope network range foreach (NetworkInterface nic in networkInterfaces) { if (nic.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties ipInterface = nic.GetIPProperties(); foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses) { if (ip.Address.AddressFamily == AddressFamily.InterNetwork) { uint addressNumber = ip.Address.ConvertIpToNumber(); if ((addressNumber & subnetMaskNumber) == networkAddressNumber) { //found address in this scope range to use as dns server _dnsServers = new IPAddress[] { ip.Address }; return; } } } } //find unicast ip address on an interface which has gateway foreach (NetworkInterface nic in networkInterfaces) { if (nic.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties ipInterface = nic.GetIPProperties(); if (ipInterface.GatewayAddresses.Count > 0) { foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses) { if (ip.Address.AddressFamily == AddressFamily.InterNetwork) { //use this address for dns _dnsServers = new IPAddress[] { ip.Address }; return; } } } } //find any unicast ip address available foreach (NetworkInterface nic in networkInterfaces) { if (nic.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties ipInterface = nic.GetIPProperties(); foreach (UnicastIPAddressInformation ip in ipInterface.UnicastAddresses) { if (ip.Address.AddressFamily == AddressFamily.InterNetwork) { //use this address for dns _dnsServers = new IPAddress[] { ip.Address }; return; } } } //no useable address was found _dnsServers = null; } internal uint GetLeaseTime() { return Convert.ToUInt32((_leaseTimeDays * 24 * 60 * 60) + (_leaseTimeHours * 60 * 60) + (_leaseTimeMinutes * 60)); } internal bool IsAddressInRange(IPAddress address) { return IsAddressInRange(address, _startingAddress, _endingAddress); } internal bool IsAddressInNetwork(IPAddress address) { uint addressNumber = address.ConvertIpToNumber(); uint networkAddressNumber = _networkAddress.ConvertIpToNumber(); uint broadcastAddressNumber = _broadcastAddress.ConvertIpToNumber(); return (networkAddressNumber < addressNumber) && (addressNumber < broadcastAddressNumber); } internal bool IsAddressExcluded(IPAddress address) { if (_exclusions != null) { foreach (Exclusion exclusion in _exclusions) { if (IsAddressInRange(address, exclusion.StartingAddress, exclusion.EndingAddress)) return true; } } return false; } internal bool IsAddressReserved(IPAddress address) { foreach (KeyValuePair reservedLease in _reservedLeases) { if (address.Equals(reservedLease.Value.Address)) return true; } return false; } internal Lease GetReservedLease(DhcpMessage request) { return GetReservedLease(new ClientIdentifierOption((byte)request.HardwareAddressType, request.ClientHardwareAddress), request.GetClientIdentifier(_ignoreClientIdentifierOption)); } private Lease GetReservedLease(ClientIdentifierOption reservedLeasesClientIdentifier, ClientIdentifierOption clientIdentifier) { if (_reservedLeases.TryGetValue(reservedLeasesClientIdentifier, out Lease reservedLease)) { //reserved address exists if (IsAddressAlreadyAllocated(reservedLease.Address, clientIdentifier)) { //reserved lease address is already allocated so ignore reserved lease _log.Write("DHCP Server cannot allocate reserved lease [" + reservedLease.Address.ToString() + "] to " + BitConverter.ToString(reservedLeasesClientIdentifier.Identifier) + " for scope '" + _name + "': The IP address is already allocated."); return null; } return reservedLease; } return null; } internal async Task GetOfferAsync(DhcpMessage request) { ClientIdentifierOption clientIdentifier = request.GetClientIdentifier(_ignoreClientIdentifierOption); if (_leases.TryGetValue(clientIdentifier, out Lease existingLease)) { //lease already exists if (existingLease.Type == LeaseType.Reserved) { Lease existingReservedLease = GetReservedLease(request); if ((existingReservedLease is not null) && (existingReservedLease.Address == existingLease.Address)) return existingLease; //return existing reserved lease //reserved lease address was changed; proceed to offer new lease } else { //is dynamic lease if (IsAddressExcluded(existingLease.Address)) { //remove existing dynamic lease; proceed to offer new lease ReleaseLease(existingLease); } else { if (_blockLocallyAdministeredMacAddresses) { if ((request.HardwareAddressType == DhcpMessageHardwareAddressType.Ethernet) && ((request.ClientHardwareAddress[0] & 0x02) > 0)) { _log.Write("DHCP Server failed to offer IP address to " + request.GetClientFullIdentifier() + " for scope '" + _name + "': the scope does not allow locally administered MAC addresses."); //prevent renewing existing dynamic lease return null; } } //return existing dynamic lease return existingLease; } } } Lease reservedLease = GetReservedLease(request); if (reservedLease != null) { Lease reservedOffer = new Lease(LeaseType.Reserved, clientIdentifier, null, request.ClientHardwareAddress, reservedLease.Address, null, GetLeaseTime()); _offers[clientIdentifier] = reservedOffer; return reservedOffer; } if (_allowOnlyReservedLeases) { _log.Write("DHCP Server failed to offer IP address to " + request.GetClientFullIdentifier() + " for scope '" + _name + "': the scope allows only reserved lease allocations."); return null; } if (_blockLocallyAdministeredMacAddresses) { if ((request.HardwareAddressType == DhcpMessageHardwareAddressType.Ethernet) && ((request.ClientHardwareAddress[0] & 0x02) > 0)) { _log.Write("DHCP Server failed to offer IP address to " + request.GetClientFullIdentifier() + " for scope '" + _name + "': the scope does not allow locally administered MAC addresses."); return null; } } Lease dummyOffer = new Lease(LeaseType.None, null, null, null, null, null, 0); Lease existingOffer = _offers.GetOrAdd(clientIdentifier, dummyOffer); if (dummyOffer != existingOffer) { if (existingOffer.Type == LeaseType.None) return null; //dummy offer so another thread is handling offer; do nothing //offer already exists existingOffer.ExtendLease(GetLeaseTime()); return existingOffer; } //find offer ip address IPAddress offerAddress = null; if (request.RequestedIpAddress != null) { //client wish to get this address IPAddress requestedAddress = request.RequestedIpAddress.Address; if (IsAddressInRange(requestedAddress)) { AddressStatus addressStatus = await IsAddressAvailableAsync(requestedAddress); if (addressStatus.IsAddressAvailable) offerAddress = requestedAddress; } } if (offerAddress is null) { await _lastAddressOfferedLock.WaitAsync(); try { //find free address from scope offerAddress = _lastAddressOffered; uint endingAddressNumber = _endingAddress.ConvertIpToNumber(); bool offerAddressWasResetFromEnd = false; while (true) { uint nextOfferAddressNumber = offerAddress.ConvertIpToNumber() + 1u; if (nextOfferAddressNumber > endingAddressNumber) { if (offerAddressWasResetFromEnd) { _log.Write("DHCP Server failed to offer IP address to " + request.GetClientFullIdentifier() + " for scope '" + _name + "': address unavailable due to address pool exhaustion."); return null; } offerAddress = IPAddressExtensions.ConvertNumberToIp(_startingAddress.ConvertIpToNumber() - 1u); offerAddressWasResetFromEnd = true; continue; } offerAddress = IPAddressExtensions.ConvertNumberToIp(nextOfferAddressNumber); AddressStatus addressStatus = await IsAddressAvailableAsync(offerAddress); if (addressStatus.IsAddressAvailable) break; if (addressStatus.NewAddress is not null) offerAddress = addressStatus.NewAddress; } _lastAddressOffered = offerAddress; } finally { _lastAddressOfferedLock.Release(); } } Lease offerLease = new Lease(LeaseType.Dynamic, clientIdentifier, null, request.ClientHardwareAddress, offerAddress, null, GetLeaseTime()); return _offers[clientIdentifier] = offerLease; } internal Lease GetExistingLeaseOrOffer(DhcpMessage request) { ClientIdentifierOption clientIdentifier = request.GetClientIdentifier(_ignoreClientIdentifierOption); //check for lease offer first since it may have a different IP address to offer if (_offers.TryGetValue(clientIdentifier, out Lease existingOffer)) return existingOffer; if (_leases.TryGetValue(clientIdentifier, out Lease existingLease)) return existingLease; return null; } internal async Task> GetOptionsAsync(DhcpMessage request, IPAddress serverIdentifierAddress, string reservedLeaseHostName, DnsServer dnsServer) { List options = new List(); switch (request.DhcpMessageType.Type) { case DhcpMessageType.Discover: options.Add(new DhcpMessageTypeOption(DhcpMessageType.Offer)); break; case DhcpMessageType.Request: case DhcpMessageType.Inform: options.Add(new DhcpMessageTypeOption(DhcpMessageType.Ack)); break; default: return null; } options.Add(new ServerIdentifierOption(serverIdentifierAddress)); switch (request.DhcpMessageType.Type) { case DhcpMessageType.Discover: case DhcpMessageType.Request: uint leaseTime = GetLeaseTime(); options.Add(new IpAddressLeaseTimeOption(leaseTime)); options.Add(new RenewalTimeValueOption(leaseTime / 2)); options.Add(new RebindingTimeValueOption(Convert.ToUInt32(leaseTime * 0.875))); break; } if (request.ParameterRequestList is null) { options.Add(new SubnetMaskOption(_subnetMask)); options.Add(new BroadcastAddressOption(_broadcastAddress)); if (!string.IsNullOrEmpty(_domainName)) { options.Add(new DomainNameOption(_domainName)); if (request.ClientFullyQualifiedDomainName != null) options.Add(GetClientFullyQualifiedDomainNameOption(request, reservedLeaseHostName)); } if (_domainSearchList is not null) options.Add(new DomainSearchOption(_domainSearchList)); if (_routerAddress is not null) options.Add(new RouterOption(new IPAddress[] { _routerAddress })); if (_dnsServers is not null) options.Add(new DomainNameServerOption(_dnsServers)); if (_winsServers is not null) options.Add(new NetBiosNameServerOption(_winsServers)); if ((_ntpServers is not null) || (_ntpServerDomainNames is not null)) options.Add(await GetNetworkTimeProtocolServersOptionAsync(dnsServer)); if (_staticRoutes is not null) options.Add(new ClasslessStaticRouteOption(_staticRoutes)); } else { foreach (DhcpOptionCode optionCode in request.ParameterRequestList.OptionCodes) { switch (optionCode) { case DhcpOptionCode.SubnetMask: options.Add(new SubnetMaskOption(_subnetMask)); options.Add(new BroadcastAddressOption(_broadcastAddress)); break; case DhcpOptionCode.HostName: if (!string.IsNullOrWhiteSpace(reservedLeaseHostName)) options.Add(new HostNameOption(reservedLeaseHostName)); break; case DhcpOptionCode.DomainName: if (!string.IsNullOrEmpty(_domainName)) { options.Add(new DomainNameOption(_domainName)); if (request.ClientFullyQualifiedDomainName != null) options.Add(GetClientFullyQualifiedDomainNameOption(request, reservedLeaseHostName)); } break; case DhcpOptionCode.DomainSearch: if (_domainSearchList is not null) options.Add(new DomainSearchOption(_domainSearchList)); break; case DhcpOptionCode.Router: if (_routerAddress is not null) options.Add(new RouterOption(new IPAddress[] { _routerAddress })); break; case DhcpOptionCode.DomainNameServer: if (_dnsServers is not null) options.Add(new DomainNameServerOption(_dnsServers)); break; case DhcpOptionCode.NetBiosOverTcpIpNameServer: if (_winsServers is not null) options.Add(new NetBiosNameServerOption(_winsServers)); break; case DhcpOptionCode.NetworkTimeProtocolServers: if ((_ntpServers is not null) || (_ntpServerDomainNames is not null)) options.Add(await GetNetworkTimeProtocolServersOptionAsync(dnsServer)); break; case DhcpOptionCode.ClasslessStaticRoute: if (_staticRoutes is not null) options.Add(new ClasslessStaticRouteOption(_staticRoutes)); break; case DhcpOptionCode.CAPWAPAccessControllerAddresses: if (_capwapAcIpAddresses is not null) options.Add(new CAPWAPAccessControllerOption(_capwapAcIpAddresses)); break; case DhcpOptionCode.TftpServerAddress: if (_tftpServerAddreses is not null) options.Add(new TftpServerAddressOption(_tftpServerAddreses)); break; default: if (_genericOptions is not null) { foreach (DhcpOption genericOption in _genericOptions) { if (optionCode == genericOption.Code) { options.Add(genericOption); break; } } } break; } } } if ((_vendorInfo is not null) && (request.VendorClassIdentifier is not null)) { if (_vendorInfo.TryGetValue(request.VendorClassIdentifier.Identifier, out VendorSpecificInformationOption vendorSpecificInformationOption) || _vendorInfo.TryGetValue("", out vendorSpecificInformationOption)) { options.Add(new VendorClassIdentifierOption(request.VendorClassIdentifier.Identifier)); options.Add(vendorSpecificInformationOption); } else { string match = "substring(vendor-class-identifier,"; foreach (KeyValuePair entry in _vendorInfo) { if (entry.Key.StartsWith(match)) { int i = entry.Key.IndexOf(')', match.Length); if (i < match.Length) continue; string[] parts = entry.Key.Substring(match.Length, i - match.Length).Split(','); if (parts.Length != 2) continue; if (!int.TryParse(parts[0], out int startIndex)) continue; if (!int.TryParse(parts[1], out int length)) continue; if ((startIndex + length) > request.VendorClassIdentifier.Identifier.Length) continue; int j = entry.Key.IndexOf("==", i); if (j < i) continue; string value = entry.Key.Substring(j + 2); value = value.Trim(); value = value.Trim('"'); if (request.VendorClassIdentifier.Identifier.Substring(startIndex, length).Equals(value)) { options.Add(new VendorClassIdentifierOption(value)); options.Add(entry.Value); break; } } } } } options.Add(DhcpOption.CreateEndOption()); return options; } private async Task GetNetworkTimeProtocolServersOptionAsync(DnsServer dnsServer) { if (_ntpServerDomainNames is not null) { Task[] tasks = new Task[_ntpServerDomainNames.Count]; int i = 0; foreach (string ntpServerDomainName in _ntpServerDomainNames) tasks[i++] = dnsServer.DirectQueryAsync(new DnsQuestionRecord(ntpServerDomainName, DnsResourceRecordType.A, DnsClass.IN), 1000); List ntpServers = new List(_ntpServerDomainNames.Count + (_ntpServers is null ? 0 : _ntpServers.Count)); if (_ntpServers is not null) ntpServers.AddRange(_ntpServers); foreach (Task task in tasks) { try { ntpServers.AddRange(DnsClient.ParseResponseA(await task)); } catch { } } return new NetworkTimeProtocolServersOption(ntpServers); } else { return new NetworkTimeProtocolServersOption(_ntpServers); } } internal void CommitLease(Lease lease) { lease.ExtendLease(GetLeaseTime()); _leases[lease.ClientIdentifier] = lease; _offers.TryRemove(lease.ClientIdentifier, out _); _lastModified = DateTime.UtcNow; } internal void ReleaseLease(Lease lease) { _leases.TryRemove(lease.ClientIdentifier, out _); _lastModified = DateTime.UtcNow; } internal void SetEnabled(bool enabled) { _enabled = enabled; if (!enabled) { _interfaceAddress = null; _interfaceIndex = 0; } } internal void RemoveExpiredOffers() { DateTime utcNow = DateTime.UtcNow; foreach (KeyValuePair offer in _offers) { if (utcNow > offer.Value.LeaseObtained.AddSeconds(OFFER_EXPIRY_SECONDS)) { //offer expired _offers.TryRemove(offer.Key, out _); } } } internal List RemoveExpiredLeases() { List expiredLeases = new List(); DateTime utcNow = DateTime.UtcNow; foreach (KeyValuePair lease in _leases) { if (utcNow > lease.Value.LeaseExpires) { //lease expired if (_leases.TryRemove(lease.Key, out Lease expiredLease)) expiredLeases.Add(expiredLease); } } if (expiredLeases.Count > 0) _lastModified = DateTime.UtcNow; return expiredLeases; } #endregion #region public public void ChangeNetwork(IPAddress startingAddress, IPAddress endingAddress, IPAddress subnetMask) { if (startingAddress.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("The address must be an IPv4 address: " + startingAddress.ToString(), nameof(startingAddress)); if (endingAddress.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("The address must be an IPv4 address: " + endingAddress.ToString(), nameof(endingAddress)); if (subnetMask.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("The address must be an IPv4 address: " + subnetMask.ToString(), nameof(subnetMask)); uint startingAddressNumber = startingAddress.ConvertIpToNumber(); uint endingAddressNumber = endingAddress.ConvertIpToNumber(); if (startingAddressNumber >= endingAddressNumber) throw new ArgumentException("Ending address must be greater than starting address."); _startingAddress = startingAddress; _endingAddress = endingAddress; _subnetMask = subnetMask; //compute other parameters uint subnetMaskNumber = _subnetMask.ConvertIpToNumber(); uint networkAddressNumber = startingAddressNumber & subnetMaskNumber; uint broadcastAddressNumber = networkAddressNumber | ~subnetMaskNumber; if (networkAddressNumber == startingAddressNumber) throw new ArgumentException("Starting address cannot be same as the network address."); if (broadcastAddressNumber == endingAddressNumber) throw new ArgumentException("Ending address cannot be same as the broadcast address."); _networkAddress = IPAddressExtensions.ConvertNumberToIp(networkAddressNumber); _broadcastAddress = IPAddressExtensions.ConvertNumberToIp(broadcastAddressNumber); _lastAddressOfferedLock.Wait(); try { _lastAddressOffered = IPAddressExtensions.ConvertNumberToIp(startingAddressNumber - 1u); } finally { _lastAddressOfferedLock.Release(); } } public bool AddReservedLease(Lease reservedLease) { return _reservedLeases.TryAdd(reservedLease.ClientIdentifier, reservedLease); } public bool RemoveReservedLease(string hardwareAddress) { byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress); ClientIdentifierOption reservedLeaseClientIdentifier = new ClientIdentifierOption((byte)DhcpMessageHardwareAddressType.Ethernet, hardwareAddressBytes); return _reservedLeases.TryRemove(reservedLeaseClientIdentifier, out _); } public Lease RemoveLease(string hardwareAddress) { byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress); foreach (KeyValuePair entry in _leases) { if (BinaryNumber.Equals(entry.Value.HardwareAddress, hardwareAddressBytes)) return RemoveLease(entry.Key); } throw new DhcpServerException("No lease was found for hardware address: " + hardwareAddress); } public Lease RemoveLease(ClientIdentifierOption clientIdentifier) { if (!_leases.TryRemove(clientIdentifier, out Lease removedLease)) throw new DhcpServerException("No lease was found for client identifier: " + clientIdentifier.ToString()); if (removedLease.Type == LeaseType.Reserved) { //remove reserved lease ClientIdentifierOption reservedLeaseClientIdentifier = new ClientIdentifierOption((byte)DhcpMessageHardwareAddressType.Ethernet, removedLease.HardwareAddress); if (_reservedLeases.TryGetValue(reservedLeaseClientIdentifier, out Lease existingReservedLease)) { //remove reserved lease only if the IP addresses match if (existingReservedLease.Address.Equals(removedLease.Address)) _reservedLeases.TryRemove(reservedLeaseClientIdentifier, out _); } } //remove DNS entries if any _dhcpServer.UpdateDnsAuthZone(false, this, removedLease); return removedLease; } public void ConvertToReservedLease(string hardwareAddress) { byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress); foreach (KeyValuePair entry in _leases) { Lease lease = entry.Value; if ((lease.Type == LeaseType.Dynamic) && BinaryNumber.Equals(lease.HardwareAddress, hardwareAddressBytes)) { ConvertToReservedLease(lease); return; } } throw new DhcpServerException("No dynamic lease was found for hardware address: " + hardwareAddress); } public void ConvertToReservedLease(ClientIdentifierOption clientIdentifier) { if (!_leases.TryGetValue(clientIdentifier, out Lease lease) || (lease.Type != LeaseType.Dynamic)) throw new DhcpServerException("No dynamic lease was found for client identifier: " + clientIdentifier.ToString()); ConvertToReservedLease(lease); } public void ConvertToDynamicLease(string hardwareAddress) { byte[] hardwareAddressBytes = Lease.ParseHardwareAddress(hardwareAddress); foreach (KeyValuePair entry in _leases) { Lease lease = entry.Value; if ((lease.Type == LeaseType.Reserved) && BinaryNumber.Equals(lease.HardwareAddress, hardwareAddressBytes)) { ConvertToDynamicLease(lease); return; } } throw new DhcpServerException("No reserved lease was found for hardware address: " + hardwareAddress); } public void ConvertToDynamicLease(ClientIdentifierOption clientIdentifier) { if (!_leases.TryGetValue(clientIdentifier, out Lease lease) || (lease.Type != LeaseType.Reserved)) throw new DhcpServerException("No reserved lease was found for client identifier: " + clientIdentifier.ToString()); ConvertToDynamicLease(lease); } public void WriteTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("SC")); bW.Write((byte)10); //version bW.WriteShortString(_name); bW.Write(_enabled); _startingAddress.WriteTo(bW); _endingAddress.WriteTo(bW); _subnetMask.WriteTo(bW); bW.Write(_leaseTimeDays); bW.Write(_leaseTimeHours); bW.Write(_leaseTimeMinutes); bW.Write(_offerDelayTime); bW.Write(_pingCheckEnabled); bW.Write(_pingCheckTimeout); bW.Write(_pingCheckRetries); if (string.IsNullOrWhiteSpace(_domainName)) bW.Write((byte)0); else bW.WriteShortString(_domainName); if (_domainSearchList is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_domainSearchList.Count)); foreach (string domainSearchString in _domainSearchList) bW.WriteShortString(domainSearchString); } bW.Write(_dnsUpdates); bW.Write(_dnsOverwriteForDynamicLease); bW.Write(_dnsTtl); if (_serverAddress is null) IPAddress.Any.WriteTo(bW); else _serverAddress.WriteTo(bW); if (string.IsNullOrEmpty(_serverHostName)) bW.Write((byte)0); else bW.WriteShortString(_serverHostName); if (string.IsNullOrEmpty(_bootFileName)) bW.Write((byte)0); else bW.WriteShortString(_bootFileName); if (_routerAddress is null) IPAddress.Any.WriteTo(bW); else _routerAddress.WriteTo(bW); if (_useThisDnsServer) { bW.Write((byte)255); } else if (_dnsServers is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_dnsServers.Count)); foreach (IPAddress dnsServer in _dnsServers) dnsServer.WriteTo(bW); } if (_winsServers is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_winsServers.Count)); foreach (IPAddress winsServer in _winsServers) winsServer.WriteTo(bW); } if (_ntpServers is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_ntpServers.Count)); foreach (IPAddress ntpServer in _ntpServers) ntpServer.WriteTo(bW); } if (_ntpServerDomainNames is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_ntpServerDomainNames.Count)); foreach (string ntpServerDomainName in _ntpServerDomainNames) bW.WriteShortString(ntpServerDomainName); } if (_staticRoutes is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_staticRoutes.Count)); foreach (ClasslessStaticRouteOption.Route route in _staticRoutes) route.WriteTo(bW.BaseStream); } if (_vendorInfo is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_vendorInfo.Count)); foreach (KeyValuePair entry in _vendorInfo) { bW.WriteShortString(entry.Key); bW.WriteBuffer(entry.Value.Information); } } if (_capwapAcIpAddresses is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_capwapAcIpAddresses.Count)); foreach (IPAddress capwapAcIpAddress in _capwapAcIpAddresses) capwapAcIpAddress.WriteTo(bW); } if (_tftpServerAddreses is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_tftpServerAddreses.Count)); foreach (IPAddress tftpServerAddress in _tftpServerAddreses) tftpServerAddress.WriteTo(bW); } if (_genericOptions is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_genericOptions.Count)); foreach (DhcpOption genericOption in _genericOptions) { bW.Write((byte)genericOption.Code); bW.Write(Convert.ToInt16(genericOption.RawValue.Length)); bW.Write(genericOption.RawValue); } } if (_exclusions is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_exclusions.Count)); foreach (Exclusion exclusion in _exclusions) { exclusion.StartingAddress.WriteTo(bW); exclusion.EndingAddress.WriteTo(bW); } } bW.Write(_reservedLeases.Count); foreach (KeyValuePair reservedLease in _reservedLeases) reservedLease.Value.WriteTo(bW); bW.Write(_allowOnlyReservedLeases); bW.Write(_blockLocallyAdministeredMacAddresses); bW.Write(_ignoreClientIdentifierOption); { bW.Write(_leases.Count); foreach (KeyValuePair lease in _leases) lease.Value.WriteTo(bW); } } public override bool Equals(object obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; return Equals(obj as Scope); } public bool Equals(Scope other) { if (other is null) return false; if (!_startingAddress.Equals(other._startingAddress)) return false; if (!_endingAddress.Equals(other._endingAddress)) return false; return true; } public override int GetHashCode() { return HashCode.Combine(_startingAddress, _endingAddress, _subnetMask); } public override string ToString() { return _name; } public int CompareTo(Scope other) { return _name.CompareTo(other._name); } #endregion #region properties public string Name { get { return _name; } set { ValidateScopeName(value); _name = value; } } public bool Enabled { get { return _enabled; } } public IPAddress StartingAddress { get { return _startingAddress; } } public IPAddress EndingAddress { get { return _endingAddress; } } public IPAddress SubnetMask { get { return _subnetMask; } } public ushort LeaseTimeDays { get { return _leaseTimeDays; } set { if (value > 999) throw new ArgumentOutOfRangeException(nameof(LeaseTimeDays), "Lease time in days must be between 0 to 999."); _leaseTimeDays = value; } } public byte LeaseTimeHours { get { return _leaseTimeHours; } set { if (value > 23) throw new ArgumentOutOfRangeException(nameof(LeaseTimeHours), "Lease time in hours must be between 0 to 23."); _leaseTimeHours = value; } } public byte LeaseTimeMinutes { get { return _leaseTimeMinutes; } set { if (value > 59) throw new ArgumentOutOfRangeException(nameof(LeaseTimeMinutes), "Lease time in minutes must be between 0 to 59."); _leaseTimeMinutes = value; } } public ushort OfferDelayTime { get { return _offerDelayTime; } set { _offerDelayTime = value; } } public bool PingCheckEnabled { get { return _pingCheckEnabled; } set { _pingCheckEnabled = value; } } public ushort PingCheckTimeout { get { return _pingCheckTimeout; } set { _pingCheckTimeout = value; } } public byte PingCheckRetries { get { return _pingCheckRetries; } set { _pingCheckRetries = value; } } public string DomainName { get { return _domainName; } set { if (value != null) DnsClient.IsDomainNameValid(value, true); _domainName = value; } } public IReadOnlyCollection DomainSearchList { get { return _domainSearchList; } set { if (value is not null) { foreach (string domainSearchString in value) DnsClient.IsDomainNameValid(domainSearchString, true); } _domainSearchList = value; } } public bool DnsUpdates { get { return _dnsUpdates; } set { _dnsUpdates = value; } } public bool DnsOverwriteForDynamicLease { get { return _dnsOverwriteForDynamicLease; } set { _dnsOverwriteForDynamicLease = value; } } public uint DnsTtl { get { return _dnsTtl; } set { _dnsTtl = value; } } public IPAddress ServerAddress { get { return _serverAddress; } set { ValidateIpv4(value, nameof(ServerAddress)); _serverAddress = value; } } public string ServerHostName { get { return _serverHostName; } set { if ((value != null) && (value.Length >= 64)) throw new ArgumentException("Server host name cannot exceed 63 bytes."); _serverHostName = value; } } public string BootFileName { get { return _bootFileName; } set { if ((value != null) && (value.Length >= 128)) throw new ArgumentException("Boot file name cannot exceed 127 bytes."); _bootFileName = value; } } public IPAddress RouterAddress { get { return _routerAddress; } set { ValidateIpv4(value, nameof(RouterAddress)); _routerAddress = value; } } public bool UseThisDnsServer { get { return _useThisDnsServer; } set { _useThisDnsServer = value; if (_useThisDnsServer) FindThisDnsServerAddress(); } } public IReadOnlyCollection DnsServers { get { return _dnsServers; } set { ValidateIpv4(value, nameof(DnsServers)); _dnsServers = value; if ((_dnsServers != null) && _dnsServers.Count > 0) _useThisDnsServer = false; } } public IReadOnlyCollection WinsServers { get { return _winsServers; } set { ValidateIpv4(value, nameof(WinsServers)); _winsServers = value; } } public IReadOnlyCollection NtpServers { get { return _ntpServers; } set { ValidateIpv4(value, nameof(NtpServers)); _ntpServers = value; } } public IReadOnlyCollection NtpServerDomainNames { get { return _ntpServerDomainNames; } set { if (value is not null) { foreach (string ntpServerDomainName in value) DnsClient.IsDomainNameValid(ntpServerDomainName, true); } _ntpServerDomainNames = value; } } public IReadOnlyCollection StaticRoutes { get { return _staticRoutes; } set { _staticRoutes = value; } } public IReadOnlyDictionary VendorInfo { get { return _vendorInfo; } set { _vendorInfo = value; } } public IReadOnlyCollection CAPWAPAcIpAddresses { get { return _capwapAcIpAddresses; } set { ValidateIpv4(value, nameof(CAPWAPAcIpAddresses)); _capwapAcIpAddresses = value; } } public IReadOnlyCollection TftpServerAddresses { get { return _tftpServerAddreses; } set { ValidateIpv4(value, nameof(TftpServerAddresses)); _tftpServerAddreses = value; } } public IReadOnlyCollection GenericOptions { get { return _genericOptions; } set { _genericOptions = value; } } public IReadOnlyCollection Exclusions { get { return _exclusions; } set { if (value is null) { _exclusions = null; } else { foreach (Exclusion exclusion in value) { if (!IsAddressInRange(exclusion.StartingAddress)) throw new ArgumentOutOfRangeException(nameof(Exclusions), "Exclusion starting address must be in scope range."); if (!IsAddressInRange(exclusion.EndingAddress)) throw new ArgumentOutOfRangeException(nameof(Exclusions), "Exclusion ending address must be in scope range."); } _exclusions = value; } } } public IReadOnlyCollection ReservedLeases { get { List leases = new List(_reservedLeases.Count); foreach (KeyValuePair entry in _reservedLeases) leases.Add(entry.Value); leases.Sort(); return leases; } set { if (value is null) { _reservedLeases.Clear(); } else { foreach (Lease reservedLease in value) { if (!IsAddressInRange(reservedLease.Address)) throw new ArgumentOutOfRangeException(nameof(ReservedLeases), "Reserved address must be in scope range."); } _reservedLeases.Clear(); foreach (Lease reservedLease in value) _reservedLeases.TryAdd(reservedLease.ClientIdentifier, reservedLease); } } } public bool AllowOnlyReservedLeases { get { return _allowOnlyReservedLeases; } set { _allowOnlyReservedLeases = value; } } public bool BlockLocallyAdministeredMacAddresses { get { return _blockLocallyAdministeredMacAddresses; } set { _blockLocallyAdministeredMacAddresses = value; } } public bool IgnoreClientIdentifierOption { get { return _ignoreClientIdentifierOption; } set { _ignoreClientIdentifierOption = value; } } public IReadOnlyDictionary Leases { get { return _leases; } } public IPAddress NetworkAddress { get { return _networkAddress; } } public IPAddress BroadcastAddress { get { return _broadcastAddress; } } public IPAddress InterfaceAddress { get { return _interfaceAddress; } } internal int InterfaceIndex { get { return _interfaceIndex; } } internal DateTime LastModified { get { return _lastModified; } } #endregion class AddressStatus { public static readonly AddressStatus TRUE = new AddressStatus(true, null); public static readonly AddressStatus FALSE = new AddressStatus(false, null); public readonly bool IsAddressAvailable; public readonly IPAddress NewAddress; public AddressStatus(bool isAddressAvailable, IPAddress newAddress) { IsAddressAvailable = isAddressAvailable; NewAddress = newAddress; } } } } ================================================ FILE: DnsServerCore/Dns/Applications/DnsApplication.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading.Tasks; namespace DnsServerCore.Dns.Applications { public sealed class DnsApplication : IDisposable { #region events public event EventHandler ConfigUpdated; #endregion #region variables readonly static Type _dnsApplicationInterface = typeof(IDnsApplication); readonly IDnsServer _dnsServer; readonly string _name; readonly DnsApplicationAssemblyLoadContext _appContext; readonly string _description; readonly Version _version; readonly IReadOnlyDictionary _dnsApplications; readonly IReadOnlyDictionary _dnsAppRecordRequestHandlers; readonly IReadOnlyDictionary _dnsRequestControllers; readonly IReadOnlyDictionary _dnsAuthoritativeRequestHandlers; readonly IReadOnlyDictionary _dnsRequestBlockingHandlers; readonly IReadOnlyDictionary _dnsQueryLoggers; readonly IReadOnlyDictionary _dnsQueryLogs; readonly IReadOnlyDictionary _dnsPostProcessors; #endregion #region constructor public DnsApplication(IDnsServer dnsServer, string name) { _dnsServer = dnsServer; _name = name; _appContext = new DnsApplicationAssemblyLoadContext(_dnsServer); //load apps Dictionary dnsApplications = new Dictionary(); Dictionary dnsAppRecordRequestHandlers = new Dictionary(2); Dictionary dnsRequestControllers = new Dictionary(1); Dictionary dnsAuthoritativeRequestHandlers = new Dictionary(1); Dictionary dnsRequestBlockingHandlers = new Dictionary(1); Dictionary dnsQueryLoggers = new Dictionary(1); Dictionary dnsQueryLogs = new Dictionary(1); Dictionary dnsPostProcessors = new Dictionary(1); foreach (Assembly appAssembly in _appContext.AppAssemblies) { try { foreach (Type classType in appAssembly.ExportedTypes) { bool isDnsApp = false; foreach (Type interfaceType in classType.GetInterfaces()) { if (interfaceType == _dnsApplicationInterface) { isDnsApp = true; break; } } if (isDnsApp) { try { IDnsApplication app = Activator.CreateInstance(classType) as IDnsApplication; dnsApplications.Add(classType.FullName, app); if (app is IDnsAppRecordRequestHandler appRecordHandler) dnsAppRecordRequestHandlers.Add(classType.FullName, appRecordHandler); if (app is IDnsRequestController requestController) dnsRequestControllers.Add(classType.FullName, requestController); if (app is IDnsAuthoritativeRequestHandler requestHandler) dnsAuthoritativeRequestHandlers.Add(classType.FullName, requestHandler); if (app is IDnsRequestBlockingHandler blockingHandler) dnsRequestBlockingHandlers.Add(classType.FullName, blockingHandler); if (app is IDnsQueryLogger logger) dnsQueryLoggers.Add(classType.FullName, logger); if (app is IDnsQueryLogs queryLogs) dnsQueryLogs.Add(classType.FullName, queryLogs); if (app is IDnsPostProcessor postProcessor) dnsPostProcessors.Add(classType.FullName, postProcessor); if (_description is null) { AssemblyDescriptionAttribute attribute = appAssembly.GetCustomAttribute(); if (attribute is not null) _description = attribute.Description.Replace("\\n", "\n"); } if (_version is null) _version = appAssembly.GetName().Version; } catch (Exception ex) { _dnsServer.WriteLog(ex); } } } } catch (Exception ex) { _dnsServer.WriteLog(ex); } } if (_version is null) { if (dnsApplications.Count > 0) _version = new Version(1, 0); else _version = new Version(0, 0); } _dnsApplications = dnsApplications; _dnsAppRecordRequestHandlers = dnsAppRecordRequestHandlers; _dnsRequestControllers = dnsRequestControllers; _dnsAuthoritativeRequestHandlers = dnsAuthoritativeRequestHandlers; _dnsRequestBlockingHandlers = dnsRequestBlockingHandlers; _dnsQueryLoggers = dnsQueryLoggers; _dnsQueryLogs = dnsQueryLogs; _dnsPostProcessors = dnsPostProcessors; } #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { if (_dnsApplications is not null) { foreach (KeyValuePair app in _dnsApplications) app.Value.Dispose(); } if (_appContext != null) _appContext.Unload(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region internal internal async Task InitializeAsync() { string config = await GetConfigAsync(); foreach (KeyValuePair app in _dnsApplications) { try { await app.Value.InitializeAsync(_dnsServer, config); } catch (Exception ex) { _dnsServer.WriteLog(ex); } } } #endregion #region public public Task GetConfigAsync() { string configFile = Path.Combine(_dnsServer.ApplicationFolder, "dnsApp.config"); if (File.Exists(configFile)) return File.ReadAllTextAsync(configFile); return Task.FromResult(null); } public async Task SetConfigAsync(string config) { string configFile = Path.Combine(_dnsServer.ApplicationFolder, "dnsApp.config"); foreach (KeyValuePair app in _dnsApplications) await app.Value.InitializeAsync(_dnsServer, config); if (string.IsNullOrEmpty(config)) File.Delete(configFile); else await File.WriteAllTextAsync(configFile, config); ConfigUpdated?.Invoke(this, EventArgs.Empty); } #endregion #region properties public IDnsServer DnsServer { get { return _dnsServer; } } public string Name { get { return _name; } } public string Description { get { return _description; } } public Version Version { get { return _version; } } public IReadOnlyDictionary DnsApplications { get { return _dnsApplications; } } public IReadOnlyDictionary DnsAppRecordRequestHandlers { get { return _dnsAppRecordRequestHandlers; } } public IReadOnlyDictionary DnsRequestControllers { get { return _dnsRequestControllers; } } public IReadOnlyDictionary DnsAuthoritativeRequestHandlers { get { return _dnsAuthoritativeRequestHandlers; } } public IReadOnlyDictionary DnsRequestBlockingHandler { get { return _dnsRequestBlockingHandlers; } } public IReadOnlyDictionary DnsQueryLoggers { get { return _dnsQueryLoggers; } } public IReadOnlyDictionary DnsQueryLogs { get { return _dnsQueryLogs; } } public IReadOnlyDictionary DnsPostProcessors { get { return _dnsPostProcessors; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Applications/DnsApplicationAssemblyLoadContext.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; namespace DnsServerCore.Dns.Applications { class DnsApplicationAssemblyLoadContext : AssemblyLoadContext { #region variables readonly IDnsServer _dnsServer; readonly List _appAssemblies; readonly AssemblyDependencyResolver _dependencyResolver; readonly Dictionary _loadedUnmanagedDlls = new Dictionary(); readonly List _dllTempPaths = new List(); #endregion #region constructor public DnsApplicationAssemblyLoadContext(IDnsServer dnsServer) : base(true) { _dnsServer = dnsServer; Unloading += delegate (AssemblyLoadContext obj) { foreach (string dllTempPath in _dllTempPaths) { try { File.Delete(dllTempPath); } catch { } } }; //load all app assemblies Dictionary appAssemblies = new Dictionary(); foreach (string depsFile in Directory.GetFiles(_dnsServer.ApplicationFolder, "*.deps.json", SearchOption.TopDirectoryOnly)) { string dllFileName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(depsFile)); string dllFile = Path.Combine(_dnsServer.ApplicationFolder, dllFileName + ".dll"); try { Assembly appAssembly; string pdbFile = Path.Combine(_dnsServer.ApplicationFolder, dllFileName + ".pdb"); if (File.Exists(pdbFile)) { using (FileStream dllStream = new FileStream(dllFile, FileMode.Open, FileAccess.Read)) { using (FileStream pdbStream = new FileStream(pdbFile, FileMode.Open, FileAccess.Read)) { appAssembly = LoadFromStream(dllStream, pdbStream); } } } else { using (FileStream dllStream = new FileStream(dllFile, FileMode.Open, FileAccess.Read)) { appAssembly = LoadFromStream(dllStream); } } appAssemblies.Add(dllFile, appAssembly); if (_dependencyResolver is null) _dependencyResolver = new AssemblyDependencyResolver(dllFile); } catch (Exception ex) { _dnsServer.WriteLog(ex); } } _appAssemblies = new List(appAssemblies.Values); } #endregion #region overrides protected override Assembly Load(AssemblyName assemblyName) { if (_dependencyResolver is not null) { string resolvedPath = _dependencyResolver.ResolveAssemblyToPath(assemblyName); if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return LoadFromAssemblyPath(GetTempDllFile(resolvedPath)); else return LoadFromAssemblyPath(resolvedPath); } } foreach (Assembly loadedAssembly in Default.Assemblies) { if (assemblyName.FullName == loadedAssembly.GetName().FullName) return loadedAssembly; } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string unmanagedDllPath = null; if (_dependencyResolver is not null) { string resolvedPath = _dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath)) unmanagedDllPath = resolvedPath; } if (unmanagedDllPath is null) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { string runtime = "win-" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); string[] prefixes = new string[] { "" }; string[] extensions = new string[] { ".dll" }; unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtime, prefixes, extensions); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { bool isAlpine = false; try { string osReleaseFile = "/etc/os-release"; if (File.Exists(osReleaseFile)) isAlpine = File.ReadAllText(osReleaseFile).Contains("alpine", StringComparison.OrdinalIgnoreCase); } catch { } string runtimeAlpine = "linux-musl-" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); string runtimeLinux = "linux-" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); string[] prefixes = new string[] { "", "lib" }; string[] extensions = new string[] { ".so", ".so.1" }; if (isAlpine) { unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtimeAlpine, prefixes, extensions); if (unmanagedDllPath is null) unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtimeLinux, prefixes, extensions); } else { unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtimeLinux, prefixes, extensions); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { string runtime = "osx-" + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); string[] prefixes = new string[] { "", "lib" }; string[] extensions = new string[] { ".dylib" }; unmanagedDllPath = FindUnmanagedDllPath(unmanagedDllName, runtime, prefixes, extensions); } if (unmanagedDllPath is null) return IntPtr.Zero; } lock (_loadedUnmanagedDlls) { if (!_loadedUnmanagedDlls.TryGetValue(unmanagedDllPath.ToLowerInvariant(), out IntPtr value)) { //load the unmanaged DLL via temp file // - to allow uninstalling/updating app at runtime on Windows // - to avoid dns server crash issue when updating apps on Linux value = LoadUnmanagedDllFromPath(GetTempDllFile(unmanagedDllPath)); _loadedUnmanagedDlls.Add(unmanagedDllPath.ToLowerInvariant(), value); } return value; } } #endregion #region private private string GetTempDllFile(string dllFile) { string tempPath = Path.GetTempFileName(); using (FileStream srcFile = new FileStream(dllFile, FileMode.Open, FileAccess.Read)) { using (FileStream dstFile = new FileStream(tempPath, FileMode.Create, FileAccess.Write)) { srcFile.CopyTo(dstFile); } } _dllTempPaths.Add(tempPath); return tempPath; } private string FindUnmanagedDllPath(string unmanagedDllName, string runtime, string[] prefixes, string[] extensions) { foreach (string prefix in prefixes) { foreach (string extension in extensions) { string path = Path.Combine(_dnsServer.ApplicationFolder, "runtimes", runtime, "native", prefix + unmanagedDllName + extension); if (File.Exists(path)) return path; } } return null; } #endregion #region properties public IReadOnlyList AppAssemblies { get { return _appAssemblies; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Applications/DnsApplicationManager.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Http.Client; namespace DnsServerCore.Dns.Applications { public sealed class DnsApplicationManager : IDisposable { #region variables readonly static Uri APP_STORE_URI = new Uri("https://go.technitium.com/?id=44"); readonly DnsServer _dnsServer; readonly string _appsPath; readonly ConcurrentDictionary _applications = new ConcurrentDictionary(); IReadOnlyList _dnsRequestControllers = []; IReadOnlyList _dnsAuthoritativeRequestHandlers = []; IReadOnlyList _dnsRequestBlockingHandlers = []; IReadOnlyList _dnsQueryLoggers = []; IReadOnlyList _dnsPostProcessors = []; string _storeAppsJsonData; DateTime _storeAppsJsonDataUpdatedOn; const int STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS = 900; Timer _appUpdateTimer; const int APP_UPDATE_TIMER_INITIAL_INTERVAL = 10000; const int APP_UPDATE_TIMER_PERIODIC_INTERVAL = 86400000; #endregion #region constructor public DnsApplicationManager(DnsServer dnsServer) { _dnsServer = dnsServer; _appsPath = Path.Combine(_dnsServer.ConfigFolder, "apps"); if (!Directory.Exists(_appsPath)) Directory.CreateDirectory(_appsPath); } #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _appUpdateTimer?.Dispose(); if (_applications != null) UnloadAllApplications(); } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region private private async Task LoadApplicationAsync(string applicationFolder, bool refreshAppObjectList) { string applicationName = Path.GetFileName(applicationFolder); DnsApplication application = new DnsApplication(new InternalDnsServer(_dnsServer, applicationName, applicationFolder), applicationName); await application.InitializeAsync(); if (!_applications.TryAdd(application.Name, application)) { application.Dispose(); throw new DnsServerException("DNS application already exists: " + application.Name); } application.ConfigUpdated += Application_ConfigUpdated; if (refreshAppObjectList) RefreshAppObjectLists(); return application; } private void UnloadApplication(string applicationName) { if (!_applications.TryRemove(applicationName, out DnsApplication removedApp)) throw new DnsServerException("DNS application does not exists: " + applicationName); RefreshAppObjectLists(); removedApp.ConfigUpdated -= Application_ConfigUpdated; removedApp.Dispose(); } private void Application_ConfigUpdated(object sender, EventArgs e) { //refresh app objects to allow sorting them as per app preference RefreshAppObjectLists(); } private void RefreshAppObjectLists() { List dnsRequestControllers = new List(1); List dnsAuthoritativeRequestHandlers = new List(1); List dnsRequestBlockingHandlers = new List(1); List dnsQueryLoggers = new List(1); List dnsPostProcessors = new List(1); foreach (KeyValuePair application in _applications) { foreach (KeyValuePair controller in application.Value.DnsRequestControllers) dnsRequestControllers.Add(controller.Value); foreach (KeyValuePair handler in application.Value.DnsAuthoritativeRequestHandlers) dnsAuthoritativeRequestHandlers.Add(handler.Value); foreach (KeyValuePair blocker in application.Value.DnsRequestBlockingHandler) dnsRequestBlockingHandlers.Add(blocker.Value); foreach (KeyValuePair logger in application.Value.DnsQueryLoggers) dnsQueryLoggers.Add(logger.Value); foreach (KeyValuePair processor in application.Value.DnsPostProcessors) dnsPostProcessors.Add(processor.Value); } //sort app objects by preference dnsRequestControllers.Sort(CompareApps); dnsAuthoritativeRequestHandlers.Sort(CompareApps); dnsRequestBlockingHandlers.Sort(CompareApps); dnsQueryLoggers.Sort(CompareApps); dnsPostProcessors.Sort(CompareApps); _dnsRequestControllers = dnsRequestControllers; _dnsAuthoritativeRequestHandlers = dnsAuthoritativeRequestHandlers; _dnsRequestBlockingHandlers = dnsRequestBlockingHandlers; _dnsQueryLoggers = dnsQueryLoggers; _dnsPostProcessors = dnsPostProcessors; } private static int CompareApps(T x, T y) { int xp; int yp; if (x is IDnsApplicationPreference xpref) xp = xpref.Preference; else xp = 100; if (y is IDnsApplicationPreference ypref) yp = ypref.Preference; else yp = 100; return xp.CompareTo(yp); } private void StartAutomaticUpdate() { if (_appUpdateTimer is null) { _appUpdateTimer = new Timer(async delegate (object state) { try { if (_applications.IsEmpty) return; _dnsServer.LogManager.Write("DNS Server has started automatic update check for DNS Apps."); string storeAppsJsonData = await GetStoreAppsJsonData(); using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData); JsonElement jsonStoreAppsArray = jsonDocument.RootElement; Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version; foreach (DnsApplication application in _applications.Values) { foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { string name = jsonStoreApp.GetProperty("name").GetString(); if (name.Equals(application.Name)) { string url = null; Version storeAppVersion = null; Version lastServerVersion = null; foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); if (currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; string version = jsonVersion.GetProperty("version").GetString(); url = jsonVersion.GetProperty("url").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; } if ((storeAppVersion is not null) && (storeAppVersion > application.Version)) { try { await DownloadAndUpdateAppAsync(application.Name, new Uri(url)); _dnsServer.LogManager.Write("DNS application '" + application.Name + "' was automatically updated successfully from: " + url); } catch (Exception ex) { _dnsServer.LogManager.Write("Failed to automatically download and update DNS application '" + application.Name + "': " + ex.ToString()); } } break; } } } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }); _appUpdateTimer.Change(APP_UPDATE_TIMER_INITIAL_INTERVAL, APP_UPDATE_TIMER_PERIODIC_INTERVAL); } } private void StopAutomaticUpdate() { if (_appUpdateTimer is not null) { _appUpdateTimer.Dispose(); _appUpdateTimer = null; } } internal async Task GetStoreAppsJsonData() { if ((_storeAppsJsonData is null) || (DateTime.UtcNow > _storeAppsJsonDataUpdatedOn.AddSeconds(STORE_APPS_JSON_DATA_CACHE_TIME_SECONDS))) { HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsServer.Proxy; handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsServer; using (HttpClient http = new HttpClient(handler)) { _storeAppsJsonData = await http.GetStringAsync(APP_STORE_URI); _storeAppsJsonDataUpdatedOn = DateTime.UtcNow; } } return _storeAppsJsonData; } #endregion #region public public void UnloadAllApplications() { foreach (KeyValuePair application in _applications) { try { application.Value.Dispose(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } _applications.Clear(); _dnsRequestControllers = Array.Empty(); _dnsAuthoritativeRequestHandlers = Array.Empty(); _dnsRequestBlockingHandlers = Array.Empty(); _dnsQueryLoggers = Array.Empty(); _dnsPostProcessors = Array.Empty(); } public async Task LoadAllApplicationsAsync() { UnloadAllApplications(); List tasks = new List(); foreach (string applicationFolder in Directory.GetDirectories(_appsPath)) { tasks.Add(Task.Run(async delegate () { try { _dnsServer.LogManager.Write("DNS Server is loading DNS application: " + Path.GetFileName(applicationFolder)); _ = await LoadApplicationAsync(applicationFolder, false); _dnsServer.LogManager.Write("DNS Server successfully loaded DNS application: " + Path.GetFileName(applicationFolder)); } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server failed to load DNS application: " + Path.GetFileName(applicationFolder) + "\r\n" + ex.ToString()); } })); } await Task.WhenAll(tasks); RefreshAppObjectLists(); } public async Task InstallApplicationAsync(string applicationName, Stream appZipStream) { foreach (char invalidChar in Path.GetInvalidFileNameChars()) { if (applicationName.Contains(invalidChar)) throw new DnsServerException("The application name contains an invalid character: " + invalidChar); } if (_applications.ContainsKey(applicationName)) throw new DnsServerException("DNS application already exists: " + applicationName); string applicationFolder = Path.Combine(_appsPath, applicationName); if (Directory.Exists(applicationFolder)) Directory.Delete(applicationFolder, true); Directory.CreateDirectory(applicationFolder); //keep a copy of the zip file in the application folder for transferring to other nodes await using (FileStream zipCopyStream = new FileStream(Path.Combine(applicationFolder, applicationName + ".zip"), FileMode.Create, FileAccess.ReadWrite)) { await appZipStream.CopyToAsync(zipCopyStream); zipCopyStream.Position = 0; using (ZipArchive appZip = new ZipArchive(zipCopyStream, ZipArchiveMode.Read, false, Encoding.UTF8)) { try { appZip.ExtractToDirectory(applicationFolder, true); return await LoadApplicationAsync(applicationFolder, true); } catch { if (Directory.Exists(applicationFolder)) Directory.Delete(applicationFolder, true); throw; } } } } public async Task UpdateApplicationAsync(string applicationName, Stream appZipStream) { if (!_applications.ContainsKey(applicationName)) throw new DnsServerException("DNS application does not exists: " + applicationName); string applicationFolder = Path.Combine(_appsPath, applicationName); //keep a copy of the zip file in the application folder for transferring to other nodes await using (FileStream zipCopyStream = new FileStream(Path.Combine(applicationFolder, applicationName + ".zip"), FileMode.Create, FileAccess.ReadWrite)) { await appZipStream.CopyToAsync(zipCopyStream); zipCopyStream.Position = 0; using (ZipArchive appZip = new ZipArchive(zipCopyStream, ZipArchiveMode.Read, false, Encoding.UTF8)) { UnloadApplication(applicationName); foreach (ZipArchiveEntry entry in appZip.Entries) { string entryPath = entry.FullName; if (Path.DirectorySeparatorChar != '/') entryPath = entryPath.Replace('/', '\\'); string filePath = Path.Combine(applicationFolder, entryPath); if ((entry.Name == "dnsApp.config") && File.Exists(filePath)) continue; //avoid overwriting existing config file Directory.CreateDirectory(Path.GetDirectoryName(filePath)); entry.ExtractToFile(filePath, true); } return await LoadApplicationAsync(applicationFolder, true); } } } public void UninstallApplication(string applicationName) { if (_applications.TryRemove(applicationName, out DnsApplication removedApp)) { RefreshAppObjectLists(); removedApp.ConfigUpdated -= Application_ConfigUpdated; removedApp.Dispose(); if (Directory.Exists(removedApp.DnsServer.ApplicationFolder)) { try { Directory.Delete(removedApp.DnsServer.ApplicationFolder, true); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } } } public async Task DownloadAndInstallAppAsync(string applicationName, Uri uri) { string tmpFile = Path.GetTempFileName(); try { await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //download to temp file HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsServer.Proxy; handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsServer; using (HttpClient http = new HttpClient(handler)) { await using (Stream httpStream = await http.GetStreamAsync(uri)) { await httpStream.CopyToAsync(fS); } } //install app fS.Position = 0; return await InstallApplicationAsync(applicationName, fS); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } } public async Task DownloadAndUpdateAppAsync(string applicationName, Uri uri) { string tmpFile = Path.GetTempFileName(); try { await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //download to temp file HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsServer.Proxy; handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsServer; using (HttpClient http = new HttpClient(handler)) { await using (Stream httpStream = await http.GetStreamAsync(uri)) { await httpStream.CopyToAsync(fS); } } //update app fS.Position = 0; return await UpdateApplicationAsync(applicationName, fS); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } } #endregion #region properties public IReadOnlyDictionary Applications { get { return _applications; } } public IReadOnlyList DnsRequestControllers { get { return _dnsRequestControllers; } } public IReadOnlyList DnsAuthoritativeRequestHandlers { get { return _dnsAuthoritativeRequestHandlers; } } public IReadOnlyList DnsRequestBlockingHandlers { get { return _dnsRequestBlockingHandlers; } } public IReadOnlyList DnsQueryLoggers { get { return _dnsQueryLoggers; } } public IReadOnlyList DnsPostProcessors { get { return _dnsPostProcessors; } } public bool EnableAutomaticUpdate { get { return _appUpdateTimer is not null; } set { if (value) StartAutomaticUpdate(); else StopAutomaticUpdate(); } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Applications/InternalDnsServer.cs ================================================ /* 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.ApplicationCommon; using System; using System.Net.Mail; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore.Dns.Applications { class InternalDnsServer : IDnsServer { #region variables readonly DnsServer _dnsServer; readonly string _applicationName; readonly string _applicationFolder; IDnsCache _dnsCache; #endregion #region constructor public InternalDnsServer(DnsServer dnsServer, string applicationName, string applicationFolder) { _dnsServer = dnsServer; _applicationName = applicationName; _applicationFolder = applicationFolder; } #endregion #region public public Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, CancellationToken cancellationToken = default) { return _dnsServer.DirectQueryAsync(question, timeout, true, cancellationToken); } public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, CancellationToken cancellationToken = default) { return _dnsServer.DirectQueryAsync(request, timeout, true, cancellationToken); } public Task ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken = default) { return DirectQueryAsync(question, cancellationToken: cancellationToken); } public void WriteLog(string message) { _dnsServer.LogManager.Write("DNS App [" + _applicationName + "]: " + message); } public void WriteLog(Exception ex) { _dnsServer.LogManager.Write("DNS App [" + _applicationName + "]: " + ex.ToString()); } #endregion #region properties public string ApplicationName { get { return _applicationName; } } public string ApplicationFolder { get { return _applicationFolder; } } public string ServerDomain { get { return _dnsServer.ServerDomain; } } public MailAddress ResponsiblePerson { get { return _dnsServer.ResponsiblePerson; } } public IDnsCache DnsCache { get { if (_dnsCache is null) _dnsCache = new ResolverDnsCache(_dnsServer, true); return _dnsCache; } } public NetProxy Proxy { get { return _dnsServer.Proxy; } } public bool PreferIPv6 { get { return _dnsServer.PreferIPv6; } } public ushort UdpPayloadSize { get { return _dnsServer.UdpPayloadSize; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/DirectDnsClient.cs ================================================ /* 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 System; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.Dns { class DirectDnsClient : DnsClient, IDnsCache { #region variables readonly DnsServer _dnsServer; #endregion #region constructor public DirectDnsClient(DnsServer dnsServer) { _dnsServer = dnsServer; //set dummy cache to avoid DnsCache from overwriting DnsResourceRecord.Tag properties which currently has GenericRecordInfo objects //caching here is also not required since DNS server already does caching Cache = this; } #endregion #region protected protected override async Task InternalResolveAsync(DnsDatagram request, Func> getValidatedResponseAsync = null, bool doNotReorderNameServers = false, CancellationToken cancellationToken = default) { DnsDatagram response = await _dnsServer.DirectQueryAsync(request, Timeout, cancellationToken: cancellationToken); //return DNSSEC validated response return await getValidatedResponseAsync(response, cancellationToken); } #endregion #region public public Task QueryAsync(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false) { return Task.FromResult(null); //no cache available } public void CacheResponse(DnsDatagram response, bool isDnssecBadCache = false, string zoneCut = null) { //do nothing to prevent caching } #endregion } } ================================================ FILE: DnsServerCore/Dns/DnsServer.cs ================================================ /* 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.ApplicationCommon; using DnsServerCore.Dns.Applications; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using System; using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Mail; using System.Net.Quic; using System.Net.Security; using System.Net.Sockets; using System.Runtime.ExceptionServices; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; 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.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Proxy; using TechnitiumLibrary.Net.ProxyProtocol; namespace DnsServerCore.Dns { #pragma warning disable CA2252 // This API requires opting into preview features #pragma warning disable CA1416 // Validate platform compatibility public enum DnsServerRecursion : byte { Deny = 0, Allow = 1, AllowOnlyForPrivateNetworks = 2, UseSpecifiedNetworkACL = 3 } public enum DnsServerBlockingType : byte { AnyAddress = 0, NxDomain = 1, CustomAddress = 2 } public sealed class DnsServer : IAsyncDisposable, IDisposable, IDnsClient { #region enum enum ServiceState { Stopped = 0, Starting = 1, Running = 2, Stopping = 3 } #endregion #region variables readonly static char[] commaSeparator = new char[] { ',' }; internal const int MAX_CNAME_HOPS = 16; internal const int SERVE_STALE_MAX_WAIT_TIME = 1800; //max time to wait before serve stale [RFC 8767] const int SERVE_STALE_TIME_DIFFERENCE = 200; //200ms before client timeout [RFC 8767] internal const int RECURSIVE_RESOLUTION_TIMEOUT = 60000; //max time that can be spent per recursive resolution task static readonly IPEndPoint IPENDPOINT_ANY_0 = new IPEndPoint(IPAddress.Any, 0); static readonly IReadOnlyCollection _aRecords = [new DnsARecordData(IPAddress.Any)]; static readonly IReadOnlyCollection _aaaaRecords = [new DnsAAAARecordData(IPAddress.IPv6Any)]; static readonly List _doqApplicationProtocols = new List() { new SslApplicationProtocol("doq") }; string _serverDomain; readonly string _configFolder; readonly string _dohwwwFolder; IReadOnlyList _localEndPoints; readonly LogManager _log; MailAddress _defaultResponsiblePerson; MailAddress _fallbackResponsiblePerson; NameServerAddress _thisServer; readonly List _udpListeners = new List(); readonly List _udpProxyListeners = new List(); readonly List _tcpListeners = new List(); readonly List _tcpProxyListeners = new List(); readonly List _tlsListeners = new List(); readonly List _quicListeners = new List(); WebApplication _dohWebService; readonly AuthZoneManager _authZoneManager; readonly AllowedZoneManager _allowedZoneManager; readonly BlockedZoneManager _blockedZoneManager; readonly BlockListZoneManager _blockListZoneManager; readonly CacheZoneManager _cacheZoneManager; readonly DnsApplicationManager _dnsApplicationManager; readonly ResolverDnsCache _dnsCache; readonly ResolverDnsCache _dnsCacheSkipDnsApps; //to prevent request reaching apps again readonly StatsManager _statsManager; IReadOnlyCollection _zoneTransferAllowedNetworks; IReadOnlyCollection _notifyAllowedNetworks; bool _preferIPv6; bool _enableUdpSocketPool; ushort _udpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; bool _dnssecValidation = true; bool _eDnsClientSubnet; byte _eDnsClientSubnetIPv4PrefixLength = 24; byte _eDnsClientSubnetIPv6PrefixLength = 56; NetworkAddress _eDnsClientSubnetIpv4Override; NetworkAddress _eDnsClientSubnetIpv6Override; //ipv4 prefix: udp, tcp IReadOnlyDictionary _qpmPrefixLimitsIPv4 = new Dictionary() { { 32, (600, 600) }, { 24, (6000, 6000) } }; //ipv6 prefix: udp, tcp IReadOnlyDictionary _qpmPrefixLimitsIPv6 = new Dictionary() { { 128, (600, 600) }, { 64, (1200, 1200) }, { 56, (6000, 6000) } }; int _qpmLimitSampleMinutes = 5; int _qpmLimitUdpTruncationPercentage = 50; //percentage of requests that are responded with TC when QPM limit exceeds for UDP (Slip) IReadOnlyCollection _qpmLimitBypassList; int _clientTimeout = 2000; int _tcpSendTimeout = 10000; int _tcpReceiveTimeout = 10000; int _quicIdleTimeout = 60000; int _quicMaxInboundStreams = 100; int _listenBacklog = 100; bool _enableDnsOverUdpProxy; bool _enableDnsOverTcpProxy; bool _enableDnsOverHttp; bool _enableDnsOverTls; bool _enableDnsOverHttps; bool _enableDnsOverHttp3; bool _enableDnsOverQuic; IReadOnlyCollection _reverseProxyNetworkACL; int _dnsOverUdpProxyPort = 538; int _dnsOverTcpProxyPort = 538; int _dnsOverHttpPort = 80; int _dnsOverTlsPort = 853; int _dnsOverHttpsPort = 443; int _dnsOverQuicPort = 853; string _dnsTlsCertificatePath; string _dnsTlsCertificatePassword; string _dnsOverHttpRealIpHeader = "X-Real-IP"; Timer _tlsCertificateUpdateTimer; const int TLS_CERTIFICATE_UPDATE_TIMER_INITIAL_INTERVAL = 60000; const int TLS_CERTIFICATE_UPDATE_TIMER_INTERVAL = 60000; DateTime _dnsTlsCertificateLastModifiedOn; SslServerAuthenticationOptions _dotSslServerAuthenticationOptions; SslServerAuthenticationOptions _doqSslServerAuthenticationOptions; SslServerAuthenticationOptions _dohSslServerAuthenticationOptions; IReadOnlyDictionary _tsigKeys; DnsServerRecursion _recursion; IReadOnlyCollection _recursionNetworkACL; bool _randomizeName; bool _qnameMinimization; int _resolverRetries = 2; int _resolverTimeout = 1500; int _resolverConcurrency = 2; int _resolverMaxStackCount = 16; bool _saveCacheToDisk = true; bool _serveStale = true; int _serveStaleMaxWaitTime = SERVE_STALE_MAX_WAIT_TIME; int _cachePrefetchEligibility = 2; int _cachePrefetchTrigger = 9; int _cachePrefetchSampleIntervalMinutes = 5; int _cachePrefetchSampleEligibilityHitsPerHour = 30; bool _enableBlocking = true; bool _allowTxtBlockingReport = true; IReadOnlyCollection _blockingBypassList; DnsServerBlockingType _blockingType = DnsServerBlockingType.NxDomain; uint _blockingAnswerTtl = 30; IReadOnlyCollection _customBlockingARecords = []; IReadOnlyCollection _customBlockingAAAARecords = []; NetProxy _proxy; IReadOnlyList _forwarders; bool _concurrentForwarding = true; int _forwarderRetries = 3; int _forwarderTimeout = 2000; int _forwarderConcurrency = 2; LogManager _resolverLog; LogManager _queryLog; Timer _cachePrefetchSamplingTimer; readonly object _cachePrefetchSamplingTimerLock = new object(); const int CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL = 5000; Timer _cachePrefetchRefreshTimer; readonly object _cachePrefetchRefreshTimerLock = new object(); const int CACHE_PREFETCH_REFRESH_TIMER_INTEVAL = 10000; IList _cacheRefreshSampleList; Timer _qpmLimitSamplingTimer; readonly object _qpmLimitSamplingTimerLock = new object(); const int QPM_LIMIT_SAMPLING_TIMER_INTERVAL = 10000; IReadOnlyDictionary> _qpmLimitClientSubnetStats; readonly IndependentTaskScheduler _queryTaskScheduler = new IndependentTaskScheduler(threadName: "QueryThreadPool"); TaskPool _resolverTaskPool; readonly IndependentTaskScheduler _resolverTaskScheduler = new IndependentTaskScheduler(priority: ThreadPriority.AboveNormal, threadName: "ResolverThreadPool"); readonly ConcurrentDictionary> _resolverTasks = new ConcurrentDictionary>(-1, 1000); volatile ServiceState _state = ServiceState.Stopped; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor static DnsServer() { //set min threads since the default value is too small { ThreadPool.GetMinThreads(out int minWorker, out int minIOC); int minThreads = Environment.ProcessorCount * 16; if (minWorker < minThreads) minWorker = minThreads; if (minIOC < minThreads) minIOC = minThreads; ThreadPool.SetMinThreads(minWorker, minIOC); } } public DnsServer(string configFolder, string dohwwwFolder, LogManager log, string serverDomain = null) : this(configFolder, dohwwwFolder, [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)], log, serverDomain) { } public DnsServer(string configFolder, string dohwwwFolder, IPEndPoint localEndPoint, LogManager log, string serverDomain = null) : this(configFolder, dohwwwFolder, [localEndPoint], log, serverDomain) { } public DnsServer(string configFolder, string dohwwwFolder, IReadOnlyList localEndPoints, LogManager log, string serverDomain = null) { if (string.IsNullOrEmpty(serverDomain)) serverDomain = Environment.MachineName.ToLowerInvariant(); if (!DnsClient.IsDomainNameValid(serverDomain) || IPAddress.TryParse(serverDomain, out _)) serverDomain = "dns-server-1"; //use this name instead since machine name is not a valid domain name _serverDomain = serverDomain; _configFolder = configFolder; _dohwwwFolder = dohwwwFolder; LocalEndPoints = localEndPoints; _log = log; ReconfigureResolverTaskPool(100); _authZoneManager = new AuthZoneManager(this); _allowedZoneManager = new AllowedZoneManager(this); _blockedZoneManager = new BlockedZoneManager(this); _blockListZoneManager = new BlockListZoneManager(this); _cacheZoneManager = new CacheZoneManager(this); _dnsApplicationManager = new DnsApplicationManager(this); _dnsCache = new ResolverDnsCache(this, false); _dnsCacheSkipDnsApps = new ResolverDnsCache(this, true); //to prevent request reaching apps again //init stats _statsManager = new StatsManager(this); //load dns cache async if (_saveCacheToDisk) { ThreadPool.QueueUserWorkItem(delegate (object state) { try { _cacheZoneManager.LoadCacheZoneFile(); } catch (Exception ex) { _log.Write("Failed to fully load DNS Cache from disk\r\n" + ex.ToString()); } }); } _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; await StopAsync(); StopTlsCertificateUpdateTimer(); _authZoneManager?.Dispose(); _cacheZoneManager?.Dispose(); _allowedZoneManager?.Dispose(); _blockedZoneManager?.Dispose(); _blockListZoneManager?.Dispose(); _dnsApplicationManager?.Dispose(); _statsManager?.Dispose(); _resolverTaskPool?.Dispose(); _queryTaskScheduler?.Dispose(); _resolverTaskScheduler?.Dispose(); lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveConfigFileInternal(); } catch (Exception ex) { _log.Write(ex); } finally { _pendingSave = false; } } } if (_saveCacheToDisk) { try { _cacheZoneManager?.SaveCacheZoneFile(); } catch (Exception ex) { _log.Write(ex); } } _disposed = true; GC.SuppressFinalize(this); } public void Dispose() { DisposeAsync().Sync(); } #endregion #region config public void LoadConfigFile() { string dnsConfigFile = Path.Combine(_configFolder, "dns.config"); try { using (FileStream fS = new FileStream(dnsConfigFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS, false); } _log.Write("DNS Server config file was loaded: " + dnsConfigFile); } catch (FileNotFoundException) { //general string serverDomain = Environment.GetEnvironmentVariable("DNS_SERVER_DOMAIN"); if (!string.IsNullOrEmpty(serverDomain)) ServerDomain = serverDomain; _dnsApplicationManager.EnableAutomaticUpdate = true; string strPreferIPv6 = Environment.GetEnvironmentVariable("DNS_SERVER_PREFER_IPV6"); if (!string.IsNullOrEmpty(strPreferIPv6)) PreferIPv6 = bool.Parse(strPreferIPv6); DnssecValidation = true; EnableUdpSocketPool = Environment.OSVersion.Platform == PlatformID.Win32NT; //optional protocols string strDnsOverHttp = Environment.GetEnvironmentVariable("DNS_SERVER_OPTIONAL_PROTOCOL_DNS_OVER_HTTP"); if (!string.IsNullOrEmpty(strDnsOverHttp)) EnableDnsOverHttp = bool.Parse(strDnsOverHttp); //recursion string strRecursion = Environment.GetEnvironmentVariable("DNS_SERVER_RECURSION"); if (!string.IsNullOrEmpty(strRecursion)) Recursion = Enum.Parse(strRecursion, true); else Recursion = DnsServerRecursion.AllowOnlyForPrivateNetworks; //default for security reasons string strRecursionNetworkACL = Environment.GetEnvironmentVariable("DNS_SERVER_RECURSION_NETWORK_ACL"); if (!string.IsNullOrEmpty(strRecursionNetworkACL)) { RecursionNetworkACL = strRecursionNetworkACL.Split(NetworkAccessControl.Parse, ','); } else { NetworkAddress[] recursionDeniedNetworks = null; NetworkAddress[] recursionAllowedNetworks = null; string strRecursionDeniedNetworks = Environment.GetEnvironmentVariable("DNS_SERVER_RECURSION_DENIED_NETWORKS"); if (!string.IsNullOrEmpty(strRecursionDeniedNetworks)) recursionDeniedNetworks = strRecursionDeniedNetworks.Split(NetworkAddress.Parse, ','); string strRecursionAllowedNetworks = Environment.GetEnvironmentVariable("DNS_SERVER_RECURSION_ALLOWED_NETWORKS"); if (!string.IsNullOrEmpty(strRecursionAllowedNetworks)) recursionAllowedNetworks = strRecursionAllowedNetworks.Split(NetworkAddress.Parse, ','); RecursionNetworkACL = AuthZoneInfo.ConvertDenyAllowToACL(recursionDeniedNetworks, recursionAllowedNetworks); } RandomizeName = false; //default false to allow resolving from bad name servers QnameMinimization = true; //default true to enable privacy feature //cache _cacheZoneManager.MaximumEntries = 10000; //blocking string strEnableBlocking = Environment.GetEnvironmentVariable("DNS_SERVER_ENABLE_BLOCKING"); if (!string.IsNullOrEmpty(strEnableBlocking)) EnableBlocking = bool.Parse(strEnableBlocking); string strAllowTxtBlockingReport = Environment.GetEnvironmentVariable("DNS_SERVER_ALLOW_TXT_BLOCKING_REPORT"); if (!string.IsNullOrEmpty(strAllowTxtBlockingReport)) AllowTxtBlockingReport = bool.Parse(strAllowTxtBlockingReport); string strBlockListUrls = Environment.GetEnvironmentVariable("DNS_SERVER_BLOCK_LIST_URLS"); if (!string.IsNullOrEmpty(strBlockListUrls)) _blockListZoneManager.BlockListUrls = strBlockListUrls.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries); //proxy & forwarders string strForwarders = Environment.GetEnvironmentVariable("DNS_SERVER_FORWARDERS"); if (!string.IsNullOrEmpty(strForwarders)) { DnsTransportProtocol forwarderProtocol; string strForwarderProtocol = Environment.GetEnvironmentVariable("DNS_SERVER_FORWARDER_PROTOCOL"); if (string.IsNullOrEmpty(strForwarderProtocol)) { forwarderProtocol = DnsTransportProtocol.Udp; } else { forwarderProtocol = Enum.Parse(strForwarderProtocol, true); if (forwarderProtocol == DnsTransportProtocol.HttpsJson) forwarderProtocol = DnsTransportProtocol.Https; } Forwarders = strForwarders.Split(delegate (string value) { NameServerAddress forwarder = NameServerAddress.Parse(value); if (forwarder.Protocol != forwarderProtocol) forwarder = forwarder.Clone(forwarderProtocol); return forwarder; }, ','); } //logging ResolverLogManager = _log; string strUseLocalTime = Environment.GetEnvironmentVariable("DNS_SERVER_LOG_USING_LOCAL_TIME"); if (!string.IsNullOrEmpty(strUseLocalTime)) _log.UseLocalTime = bool.Parse(strUseLocalTime); _statsManager.EnableInMemoryStats = false; _statsManager.MaxStatFileDays = 365; SaveConfigFileInternal(); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading DNS config file: " + dnsConfigFile + "\r\n" + ex.ToString()); _log.Write("Note: You may try deleting the DNS config file to fix this issue. However, you will lose DNS settings but, other data wont be affected."); } } public void LoadConfig(Stream s, bool isConfigTransfer) { lock (_saveLock) { ReadConfigFrom(s, isConfigTransfer); //save config file SaveConfigFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } internal void SaveConfigFileInternal() { string configFile = Path.Combine(_configFolder, "dns.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("DNS Server 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 ReadConfigFrom(Stream s, bool isConfigTransfer) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DC") //format throw new InvalidDataException("DNS Server config file format is invalid."); int version = bR.ReadByte(); if ((version < 1) || (version > 2)) throw new InvalidDataException("DNS Server config version not supported."); //general string serverDomain = bR.ReadShortString(); if (!isConfigTransfer) { try { ServerDomain = serverDomain; } catch { //server domain failed validation _serverDomain = serverDomain; } } { IPEndPoint[] localEndPoints; int count = bR.ReadByte(); if (count > 0) { IPEndPoint[] localEPs = new IPEndPoint[count]; for (int i = 0; i < count; i++) localEPs[i] = (IPEndPoint)EndPointExtensions.ReadFrom(bR); localEndPoints = localEPs; } else { localEndPoints = [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)]; } if (!isConfigTransfer) _localEndPoints = localEndPoints; } NetworkAddress[] ipv4SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR); if (!isConfigTransfer) DnsClientConnection.IPv4SourceAddresses = ipv4SourceAddresses; NetworkAddress[] ipv6SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR); if (!isConfigTransfer) DnsClientConnection.IPv6SourceAddresses = ipv6SourceAddresses; _authZoneManager.DefaultRecordTtl = bR.ReadUInt32(); if (version >= 2) { _authZoneManager.DefaultNsRecordTtl = bR.ReadUInt32(); _authZoneManager.DefaultSoaRecordTtl = bR.ReadUInt32(); } else { _authZoneManager.DefaultNsRecordTtl = 14400; _authZoneManager.DefaultSoaRecordTtl = 900; } string rp = bR.ReadString(); if (rp.Length == 0) _defaultResponsiblePerson = null; else _defaultResponsiblePerson = new MailAddress(rp); _authZoneManager.UseSoaSerialDateScheme = bR.ReadBoolean(); _authZoneManager.MinSoaRefresh = bR.ReadUInt32(); _authZoneManager.MinSoaRetry = bR.ReadUInt32(); _zoneTransferAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR); _notifyAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR); _dnsApplicationManager.EnableAutomaticUpdate = bR.ReadBoolean(); bool preferIPv6 = bR.ReadBoolean(); if (!isConfigTransfer) _preferIPv6 = preferIPv6; { bool enableUdpSocketPool = bR.ReadBoolean(); if (!isConfigTransfer) _enableUdpSocketPool = enableUdpSocketPool; int count = bR.ReadUInt16(); ushort[] socketPoolExcludedPorts = new ushort[count]; for (int i = 0; i < count; i++) socketPoolExcludedPorts[i] = bR.ReadUInt16(); if (!isConfigTransfer) UdpClientConnection.SocketPoolExcludedPorts = socketPoolExcludedPorts; } _udpPayloadSize = bR.ReadUInt16(); _dnssecValidation = bR.ReadBoolean(); _eDnsClientSubnet = bR.ReadBoolean(); _eDnsClientSubnetIPv4PrefixLength = bR.ReadByte(); _eDnsClientSubnetIPv6PrefixLength = bR.ReadByte(); if (bR.ReadBoolean()) _eDnsClientSubnetIpv4Override = NetworkAddress.ReadFrom(bR); else _eDnsClientSubnetIpv4Override = null; if (bR.ReadBoolean()) _eDnsClientSubnetIpv6Override = NetworkAddress.ReadFrom(bR); else _eDnsClientSubnetIpv6Override = null; { int count = bR.ReadByte(); Dictionary qpmPrefixLimitsIPv4 = new Dictionary(count); for (int i = 0; i < count; i++) qpmPrefixLimitsIPv4.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32())); _qpmPrefixLimitsIPv4 = qpmPrefixLimitsIPv4; } { int count = bR.ReadByte(); Dictionary qpmPrefixLimitsIPv6 = new Dictionary(count); for (int i = 0; i < count; i++) qpmPrefixLimitsIPv6.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32())); _qpmPrefixLimitsIPv6 = qpmPrefixLimitsIPv6; } _qpmLimitSampleMinutes = bR.ReadInt32(); _qpmLimitUdpTruncationPercentage = bR.ReadInt32(); _qpmLimitBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR); _clientTimeout = bR.ReadInt32(); _tcpSendTimeout = bR.ReadInt32(); _tcpReceiveTimeout = bR.ReadInt32(); _quicIdleTimeout = bR.ReadInt32(); _quicMaxInboundStreams = bR.ReadInt32(); _listenBacklog = bR.ReadInt32(); MaxConcurrentResolutionsPerCore = bR.ReadUInt16(); //optional protocols bool enableDnsOverUdpProxy = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverUdpProxy = enableDnsOverUdpProxy; bool enableDnsOverTcpProxy = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverTcpProxy = enableDnsOverTcpProxy; bool enableDnsOverHttp = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverHttp = enableDnsOverHttp; bool enableDnsOverTls = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverTls = enableDnsOverTls; bool enableDnsOverHttps = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverHttps = enableDnsOverHttps; bool enableDnsOverHttp3 = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverHttp3 = enableDnsOverHttp3; bool enableDnsOverQuic = bR.ReadBoolean(); if (!isConfigTransfer) _enableDnsOverQuic = enableDnsOverQuic; int dnsOverUdpProxyPort = bR.ReadInt32(); if (!isConfigTransfer) _dnsOverUdpProxyPort = dnsOverUdpProxyPort; int dnsOverTcpProxyPort = bR.ReadInt32(); if (!isConfigTransfer) _dnsOverTcpProxyPort = dnsOverTcpProxyPort; int dnsOverHttpPort = bR.ReadInt32(); if (!isConfigTransfer) _dnsOverHttpPort = dnsOverHttpPort; int dnsOverTlsPort = bR.ReadInt32(); if (!isConfigTransfer) _dnsOverTlsPort = dnsOverTlsPort; int dnsOverHttpsPort = bR.ReadInt32(); if (!isConfigTransfer) _dnsOverHttpsPort = dnsOverHttpsPort; int dnsOverQuicPort = bR.ReadInt32(); if (!isConfigTransfer) _dnsOverQuicPort = dnsOverQuicPort; NetworkAccessControl[] reverseProxyNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR); if (!isConfigTransfer) _reverseProxyNetworkACL = reverseProxyNetworkACL; string dnsTlsCertificatePath = bR.ReadShortString(); string dnsTlsCertificatePassword = bR.ReadShortString(); if (!isConfigTransfer) { _dnsTlsCertificatePath = dnsTlsCertificatePath; _dnsTlsCertificatePassword = dnsTlsCertificatePassword; if (_dnsTlsCertificatePath.Length == 0) _dnsTlsCertificatePath = null; if (_dnsTlsCertificatePath is null) { StopTlsCertificateUpdateTimer(); } else { string dnsTlsCertificateAbsolutePath = ConvertToAbsolutePath(_dnsTlsCertificatePath); try { LoadDnsTlsCertificate(dnsTlsCertificateAbsolutePath, _dnsTlsCertificatePassword); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading DNS Server TLS certificate: " + dnsTlsCertificateAbsolutePath + "\r\n" + ex.ToString()); } StartTlsCertificateUpdateTimer(); } } string dnsOverHttpRealIpHeader = bR.ReadShortString(); if (!isConfigTransfer) _dnsOverHttpRealIpHeader = dnsOverHttpRealIpHeader; //tsig { int count = bR.ReadByte(); Dictionary tsigKeys = new Dictionary(count); for (int i = 0; i < count; i++) { string keyName = bR.ReadShortString(); string sharedSecret = bR.ReadShortString(); TsigAlgorithm algorithm = (TsigAlgorithm)bR.ReadByte(); tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithm)); } _tsigKeys = tsigKeys; } //recursion _recursion = (DnsServerRecursion)bR.ReadByte(); _recursionNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR); _randomizeName = bR.ReadBoolean(); _qnameMinimization = bR.ReadBoolean(); _resolverRetries = bR.ReadInt32(); _resolverTimeout = bR.ReadInt32(); _resolverConcurrency = bR.ReadInt32(); _resolverMaxStackCount = bR.ReadInt32(); //cache bool saveCacheToDisk = bR.ReadBoolean(); if (!isConfigTransfer) _saveCacheToDisk = saveCacheToDisk; bool serveStale = bR.ReadBoolean(); if (!isConfigTransfer) _serveStale = serveStale; uint serveStaleTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.ServeStaleTtl = serveStaleTtl; uint serveStaleAnswerTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.ServeStaleAnswerTtl = serveStaleAnswerTtl; uint serveStaleResetTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.ServeStaleResetTtl = serveStaleResetTtl; int serveStaleMaxWaitTime = bR.ReadInt32(); if (!isConfigTransfer) _serveStaleMaxWaitTime = serveStaleMaxWaitTime; long cacheMaximumEntries = bR.ReadInt64(); if (!isConfigTransfer) _cacheZoneManager.MaximumEntries = cacheMaximumEntries; uint minimumRecordTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.MinimumRecordTtl = minimumRecordTtl; uint maximumRecordTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.MaximumRecordTtl = maximumRecordTtl; uint negativeRecordTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.NegativeRecordTtl = negativeRecordTtl; uint failureRecordTtl = bR.ReadUInt32(); if (!isConfigTransfer) _cacheZoneManager.FailureRecordTtl = failureRecordTtl; int cachePrefetchEligibility = bR.ReadInt32(); if (!isConfigTransfer) _cachePrefetchEligibility = cachePrefetchEligibility; int cachePrefetchTrigger = bR.ReadInt32(); if (!isConfigTransfer) _cachePrefetchTrigger = cachePrefetchTrigger; int cachePrefetchSampleIntervalMinutes = bR.ReadInt32(); if (!isConfigTransfer) _cachePrefetchSampleIntervalMinutes = cachePrefetchSampleIntervalMinutes; int cachePrefetchSampleEligibilityHitsPerHour = bR.ReadInt32(); if (!isConfigTransfer) _cachePrefetchSampleEligibilityHitsPerHour = cachePrefetchSampleEligibilityHitsPerHour; //blocking _enableBlocking = bR.ReadBoolean(); _allowTxtBlockingReport = bR.ReadBoolean(); _blockingBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR); _blockingType = (DnsServerBlockingType)bR.ReadByte(); { //read custom blocking addresses List dnsARecords = new List(); List dnsAAAARecords = new List(); int count = bR.ReadByte(); if (count > 0) { for (int i = 0; i < count; i++) { IPAddress customAddress = IPAddressExtensions.ReadFrom(bR); switch (customAddress.AddressFamily) { case AddressFamily.InterNetwork: dnsARecords.Add(new DnsARecordData(customAddress)); break; case AddressFamily.InterNetworkV6: dnsAAAARecords.Add(new DnsAAAARecordData(customAddress)); break; } } } _customBlockingARecords = dnsARecords; _customBlockingAAAARecords = dnsAAAARecords; } _blockingAnswerTtl = bR.ReadUInt32(); //proxy & forwarders NetProxyType proxyType = (NetProxyType)bR.ReadByte(); if (proxyType != NetProxyType.None) { string address = bR.ReadShortString(); int port = bR.ReadInt32(); NetworkCredential credential = null; if (bR.ReadBoolean()) //credential set credential = new NetworkCredential(bR.ReadShortString(), bR.ReadShortString()); _proxy = NetProxy.CreateProxy(proxyType, address, port, credential); int count = bR.ReadByte(); List bypassList = new List(count); for (int i = 0; i < count; i++) bypassList.Add(new NetProxyBypassItem(bR.ReadShortString())); _proxy.BypassList = bypassList; } else { _proxy = null; } { int count = bR.ReadByte(); if (count > 0) { NameServerAddress[] forwarders = new NameServerAddress[count]; for (int i = 0; i < count; i++) { forwarders[i] = new NameServerAddress(bR); if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson) forwarders[i] = forwarders[i].Clone(DnsTransportProtocol.Https); } _forwarders = forwarders; } else { _forwarders = null; } } _concurrentForwarding = bR.ReadBoolean(); _forwarderRetries = bR.ReadInt32(); _forwarderTimeout = bR.ReadInt32(); _forwarderConcurrency = bR.ReadInt32(); //logging bool ignoreResolverLogs = bR.ReadBoolean(); //ignore resolver logs if (!isConfigTransfer) { if (ignoreResolverLogs) _resolverLog = null; else _resolverLog = _log; } bool logQueries = bR.ReadBoolean(); //log all queries if (!isConfigTransfer) { if (logQueries) _queryLog = _log; else _queryLog = null; } bool enableInMemoryStats = bR.ReadBoolean(); if (!isConfigTransfer) _statsManager.EnableInMemoryStats = enableInMemoryStats; int maxStatFileDays = bR.ReadInt32(); if (!isConfigTransfer) _statsManager.MaxStatFileDays = maxStatFileDays; } private void WriteConfigTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("DC")); //format bW.Write((byte)2); //version //general bW.WriteShortString(_serverDomain); { bW.Write(Convert.ToByte(_localEndPoints.Count)); foreach (IPEndPoint localEP in _localEndPoints) localEP.WriteTo(bW); } AuthZoneInfo.WriteNetworkAddressesTo(DnsClientConnection.IPv4SourceAddresses, bW); AuthZoneInfo.WriteNetworkAddressesTo(DnsClientConnection.IPv6SourceAddresses, bW); bW.Write(_authZoneManager.DefaultRecordTtl); bW.Write(_authZoneManager.DefaultNsRecordTtl); bW.Write(_authZoneManager.DefaultSoaRecordTtl); if (_defaultResponsiblePerson is null) bW.WriteShortString(""); else bW.WriteShortString(_defaultResponsiblePerson.Address); bW.Write(_authZoneManager.UseSoaSerialDateScheme); bW.Write(_authZoneManager.MinSoaRefresh); bW.Write(_authZoneManager.MinSoaRetry); AuthZoneInfo.WriteNetworkAddressesTo(_zoneTransferAllowedNetworks, bW); AuthZoneInfo.WriteNetworkAddressesTo(_notifyAllowedNetworks, bW); bW.Write(_dnsApplicationManager.EnableAutomaticUpdate); bW.Write(_preferIPv6); bW.Write(_enableUdpSocketPool); ushort[] socketPoolExcludedPorts = UdpClientConnection.SocketPoolExcludedPorts; if (socketPoolExcludedPorts is null) { bW.Write(ushort.MinValue); } else { bW.Write(Convert.ToUInt16(socketPoolExcludedPorts.Length)); foreach (ushort excludedPort in socketPoolExcludedPorts) bW.Write(excludedPort); } bW.Write(_udpPayloadSize); bW.Write(_dnssecValidation); bW.Write(_eDnsClientSubnet); bW.Write(_eDnsClientSubnetIPv4PrefixLength); bW.Write(_eDnsClientSubnetIPv6PrefixLength); if (_eDnsClientSubnetIpv4Override is null) { bW.Write(false); } else { bW.Write(true); _eDnsClientSubnetIpv4Override.WriteTo(bW); } if (_eDnsClientSubnetIpv6Override is null) { bW.Write(false); } else { bW.Write(true); _eDnsClientSubnetIpv6Override.WriteTo(bW); } if (_qpmPrefixLimitsIPv4.Count == 0) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_qpmPrefixLimitsIPv4.Count)); foreach (KeyValuePair qpmPrefixLimit in _qpmPrefixLimitsIPv4) { bW.Write(qpmPrefixLimit.Key); bW.Write(qpmPrefixLimit.Value.Item1); bW.Write(qpmPrefixLimit.Value.Item2); } } if (_qpmPrefixLimitsIPv6.Count == 0) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_qpmPrefixLimitsIPv6.Count)); foreach (KeyValuePair qpmPrefixLimit in _qpmPrefixLimitsIPv6) { bW.Write(qpmPrefixLimit.Key); bW.Write(qpmPrefixLimit.Value.Item1); bW.Write(qpmPrefixLimit.Value.Item2); } } bW.Write(_qpmLimitSampleMinutes); bW.Write(_qpmLimitUdpTruncationPercentage); AuthZoneInfo.WriteNetworkAddressesTo(_qpmLimitBypassList, bW); bW.Write(_clientTimeout); bW.Write(_tcpSendTimeout); bW.Write(_tcpReceiveTimeout); bW.Write(_quicIdleTimeout); bW.Write(_quicMaxInboundStreams); bW.Write(_listenBacklog); bW.Write(MaxConcurrentResolutionsPerCore); //optional protocols bW.Write(_enableDnsOverUdpProxy); bW.Write(_enableDnsOverTcpProxy); bW.Write(_enableDnsOverHttp); bW.Write(_enableDnsOverTls); bW.Write(_enableDnsOverHttps); bW.Write(_enableDnsOverHttp3); bW.Write(_enableDnsOverQuic); bW.Write(_dnsOverUdpProxyPort); bW.Write(_dnsOverTcpProxyPort); bW.Write(_dnsOverHttpPort); bW.Write(_dnsOverTlsPort); bW.Write(_dnsOverHttpsPort); bW.Write(_dnsOverQuicPort); AuthZoneInfo.WriteNetworkACLTo(_reverseProxyNetworkACL, bW); if (_dnsTlsCertificatePath == null) bW.WriteShortString(string.Empty); else bW.WriteShortString(_dnsTlsCertificatePath); if (_dnsTlsCertificatePassword == null) bW.WriteShortString(string.Empty); else bW.WriteShortString(_dnsTlsCertificatePassword); bW.WriteShortString(_dnsOverHttpRealIpHeader); //tsig if (_tsigKeys is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_tsigKeys.Count)); foreach (KeyValuePair tsigKey in _tsigKeys) { bW.WriteShortString(tsigKey.Key); bW.WriteShortString(tsigKey.Value.SharedSecret); bW.Write((byte)tsigKey.Value.Algorithm); } } //recursion bW.Write((byte)_recursion); AuthZoneInfo.WriteNetworkACLTo(_recursionNetworkACL, bW); bW.Write(_randomizeName); bW.Write(_qnameMinimization); bW.Write(_resolverRetries); bW.Write(_resolverTimeout); bW.Write(_resolverConcurrency); bW.Write(_resolverMaxStackCount); //cache bW.Write(_saveCacheToDisk); bW.Write(_serveStale); bW.Write(_cacheZoneManager.ServeStaleTtl); bW.Write(_cacheZoneManager.ServeStaleAnswerTtl); bW.Write(_cacheZoneManager.ServeStaleResetTtl); bW.Write(_serveStaleMaxWaitTime); bW.Write(_cacheZoneManager.MaximumEntries); bW.Write(_cacheZoneManager.MinimumRecordTtl); bW.Write(_cacheZoneManager.MaximumRecordTtl); bW.Write(_cacheZoneManager.NegativeRecordTtl); bW.Write(_cacheZoneManager.FailureRecordTtl); bW.Write(_cachePrefetchEligibility); bW.Write(_cachePrefetchTrigger); bW.Write(_cachePrefetchSampleIntervalMinutes); bW.Write(_cachePrefetchSampleEligibilityHitsPerHour); //blocking bW.Write(_enableBlocking); bW.Write(_allowTxtBlockingReport); AuthZoneInfo.WriteNetworkAddressesTo(_blockingBypassList, bW); bW.Write((byte)_blockingType); { bW.Write(Convert.ToByte(_customBlockingARecords.Count + _customBlockingAAAARecords.Count)); foreach (DnsARecordData record in _customBlockingARecords) record.Address.WriteTo(bW); foreach (DnsAAAARecordData record in _customBlockingAAAARecords) record.Address.WriteTo(bW); } bW.Write(_blockingAnswerTtl); //proxy & forwarders if (_proxy == null) { bW.Write((byte)NetProxyType.None); } else { bW.Write((byte)_proxy.Type); bW.WriteShortString(_proxy.Address); bW.Write(_proxy.Port); NetworkCredential credential = _proxy.Credential; if (credential == null) { bW.Write(false); } else { bW.Write(true); bW.WriteShortString(credential.UserName); bW.WriteShortString(credential.Password); } //bypass list { bW.Write(Convert.ToByte(_proxy.BypassList.Count)); foreach (NetProxyBypassItem item in _proxy.BypassList) bW.WriteShortString(item.Value); } } if (_forwarders == null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_forwarders.Count)); foreach (NameServerAddress forwarder in _forwarders) forwarder.WriteTo(bW); } bW.Write(_concurrentForwarding); bW.Write(_forwarderRetries); bW.Write(_forwarderTimeout); bW.Write(_forwarderConcurrency); //logging bW.Write(_resolverLog is null); //ignore resolver logs bW.Write(_queryLog is not null); //log all queries bW.Write(_statsManager.EnableInMemoryStats); bW.Write(_statsManager.MaxStatFileDays); } #endregion #region tls private void StartTlsCertificateUpdateTimer() { if (_tlsCertificateUpdateTimer is null) { _tlsCertificateUpdateTimer = new Timer(delegate (object state) { if (!string.IsNullOrEmpty(_dnsTlsCertificatePath)) { string dnsTlsCertificatePath = ConvertToAbsolutePath(_dnsTlsCertificatePath); try { FileInfo fileInfo = new FileInfo(dnsTlsCertificatePath); if (fileInfo.Exists && (fileInfo.LastWriteTimeUtc != _dnsTlsCertificateLastModifiedOn)) LoadDnsTlsCertificate(dnsTlsCertificatePath, _dnsTlsCertificatePassword); } catch (Exception ex) { _log.Write("DNS Server encountered an error while updating DNS Server TLS Certificate: " + dnsTlsCertificatePath + "\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 LoadDnsTlsCertificate(string tlsCertificatePath, string tlsCertificatePassword) { FileInfo fileInfo = new FileInfo(tlsCertificatePath); if (!fileInfo.Exists) throw new ArgumentException("DNS Server TLS certificate file does not exists: " + tlsCertificatePath); switch (Path.GetExtension(tlsCertificatePath).ToLowerInvariant()) { case ".pfx": case ".p12": break; default: throw new ArgumentException("DNS Server 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("DNS Server TLS certificate file must contain a certificate with private key."); SslStreamCertificateContext certificateContext = SslStreamCertificateContext.Create(serverCertificate, certificateCollection, false); _dotSslServerAuthenticationOptions = new SslServerAuthenticationOptions() { ServerCertificateContext = certificateContext }; _doqSslServerAuthenticationOptions = new SslServerAuthenticationOptions() { ApplicationProtocols = _doqApplicationProtocols, ServerCertificateContext = certificateContext }; List applicationProtocols = new List(); if (_enableDnsOverHttp3) applicationProtocols.Add(new SslApplicationProtocol("h3")); if (IsHttp2Supported()) applicationProtocols.Add(new SslApplicationProtocol("h2")); applicationProtocols.Add(new SslApplicationProtocol("http/1.1")); _dohSslServerAuthenticationOptions = new SslServerAuthenticationOptions { ApplicationProtocols = applicationProtocols, ServerCertificateContext = certificateContext, }; _dnsTlsCertificateLastModifiedOn = fileInfo.LastWriteTimeUtc; _log.Write("DNS Server TLS certificate was loaded: " + tlsCertificatePath); } public void RemoveDnsTlsCertificate() { _dotSslServerAuthenticationOptions = null; _doqSslServerAuthenticationOptions = null; _dohSslServerAuthenticationOptions = null; _dnsTlsCertificatePath = null; _dnsTlsCertificatePassword = null; StopTlsCertificateUpdateTimer(); } public void SetDnsTlsCertificate(string dnsTlsCertificatePath, string dnsTlsCertificatePassword = null) { if (string.IsNullOrEmpty(dnsTlsCertificatePath)) throw new ArgumentNullException(nameof(dnsTlsCertificatePath), "DNS optional protocols TLS certificate path cannot be null or empty."); if (dnsTlsCertificatePath.Length > 255) throw new ArgumentException("DNS optional protocols TLS certificate path length cannot exceed 255 characters.", nameof(dnsTlsCertificatePath)); if (dnsTlsCertificatePassword?.Length > 255) throw new ArgumentException("DNS optional protocols TLS certificate password length cannot exceed 255 characters.", nameof(dnsTlsCertificatePassword)); dnsTlsCertificatePath = ConvertToAbsolutePath(dnsTlsCertificatePath); try { LoadDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword); } catch (Exception ex) { _log.Write("DNS Server encountered an error while loading DNS Server TLS certificate: " + dnsTlsCertificatePath + "\r\n" + ex.ToString()); } _dnsTlsCertificatePath = ConvertToRelativePath(dnsTlsCertificatePath); _dnsTlsCertificatePassword = dnsTlsCertificatePassword; StartTlsCertificateUpdateTimer(); } 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 private private async Task ReadUdpRequestAsync(Socket udpListener, DnsTransportProtocol protocol) { bool sendTruncationResponse; byte[] recvBuffer; if (protocol == DnsTransportProtocol.UdpProxy) recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE + 256]; else recvBuffer = new byte[DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE]; using MemoryStream recvBufferStream = new MemoryStream(recvBuffer); try { int localPort = (udpListener.LocalEndPoint as IPEndPoint).Port; EndPoint epAny; switch (udpListener.AddressFamily) { case AddressFamily.InterNetwork: epAny = new IPEndPoint(IPAddress.Any, 0); break; case AddressFamily.InterNetworkV6: epAny = new IPEndPoint(IPAddress.IPv6Any, 0); break; default: throw new NotSupportedException("AddressFamily not supported."); } SocketReceiveMessageFromResult result; while (true) { recvBufferStream.SetLength(DnsDatagram.EDNS_MAX_UDP_PAYLOAD_SIZE); //resetting length before using buffer try { result = await udpListener.ReceiveMessageFromAsync(recvBuffer, SocketFlags.None, epAny); } catch (SocketException ex) { switch (ex.SocketErrorCode) { case SocketError.ConnectionReset: case SocketError.HostUnreachable: case SocketError.MessageSize: case SocketError.NetworkReset: result = default; break; default: throw; } } if (result.ReceivedBytes > 0) { if (result.RemoteEndPoint is not IPEndPoint remoteEP) continue; try { recvBufferStream.Position = 0; recvBufferStream.SetLength(result.ReceivedBytes); IPEndPoint returnEP = remoteEP; if (protocol == DnsTransportProtocol.UdpProxy) { if (!NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL)) { //this feature is intended to be used with a reverse proxy or load balancer on private network continue; } ProxyProtocolStream proxyStream = await ProxyProtocolStream.CreateAsServerAsync(recvBufferStream); if (!proxyStream.IsLocal) remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort); recvBufferStream.Position = proxyStream.DataOffset; } if (HasQpmLimitExceeded(remoteEP.Address, DnsTransportProtocol.Udp)) { if (SendQpmLimitExceededTruncationResponse()) { sendTruncationResponse = true; } else { _statsManager.QueueUpdate(null, remoteEP, protocol, null, true); continue; } } else { sendTruncationResponse = false; } DnsDatagram request = DnsDatagram.ReadFrom(recvBufferStream); request.SetMetadata(new NameServerAddress(new IPEndPoint(result.PacketInformation.Address, localPort), DnsTransportProtocol.Udp)); _ = ProcessUdpRequestAsync(udpListener, remoteEP, returnEP, protocol, request, sendTruncationResponse); } catch (EndOfStreamException) { //ignore incomplete udp datagrams } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } } } catch (ObjectDisposedException) { //server stopped } catch (SocketException ex) { switch (ex.SocketErrorCode) { case SocketError.OperationAborted: case SocketError.Interrupted: break; //server stopping default: if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(ex); break; } } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(ex); } } private async Task ProcessUdpRequestAsync(Socket udpListener, IPEndPoint remoteEP, IPEndPoint returnEP, DnsTransportProtocol protocol, DnsDatagram request, bool sendTruncationResponse) { byte[] sendBuffer = null; try { bool recursionAllowed = IsRecursionAllowed(remoteEP.Address); DnsDatagram response; if (sendTruncationResponse) { response = new DnsDatagram(request.Identifier, true, request.OPCODE, false, true, request.RecursionDesired, recursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NoError, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; } else { response = await ProcessRequestAsync(request, remoteEP, protocol, recursionAllowed); if (response is null) { _statsManager.QueueUpdate(null, remoteEP, protocol, null, false); return; //drop request } } //send response int sendBufferSize; if (request.EDNS is null) sendBufferSize = 512; else if (request.EDNS.UdpPayloadSize > _udpPayloadSize) sendBufferSize = _udpPayloadSize; else sendBufferSize = request.EDNS.UdpPayloadSize; sendBuffer = ArrayPool.Shared.Rent(sendBufferSize); using (MemoryStream sendBufferStream = new MemoryStream(sendBuffer, 0, sendBufferSize)) { try { response.WriteTo(sendBufferStream); } catch (NotSupportedException) { if (response.IsSigned) { //rfc8945 section 5.3 response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, null, null, new DnsResourceRecord[] { response.Additional[response.Additional.Count - 1] }, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; } else { switch (response.Question[0].Type) { case DnsResourceRecordType.MX: case DnsResourceRecordType.SRV: case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: //removing glue records and trying again since some mail servers fail to fallback to TCP on truncation //removing glue records to prevent truncation for SRV/SVCB/HTTPS response = response.CloneWithoutGlueRecords(); sendBufferStream.Position = 0; try { response.WriteTo(sendBufferStream); } catch (NotSupportedException) { //send TC since response is still big even after removing glue records response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; } break; case DnsResourceRecordType.IXFR: response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, false, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, new DnsResourceRecord[] { response.Answer[0] }, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; //truncate response break; default: response = new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, true, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, response.RCODE, response.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize) { Tag = DnsServerResponseType.Authoritative }; break; } } sendBufferStream.Position = 0; response.WriteTo(sendBufferStream); } //send dns datagram async await udpListener.SendToAsync(new ArraySegment(sendBuffer, 0, (int)sendBufferStream.Position), SocketFlags.None, returnEP); } _queryLog?.Write(remoteEP, protocol, request, response); _statsManager.QueueUpdate(request, remoteEP, protocol, response, false); } catch (ObjectDisposedException) { //ignore } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _queryLog?.Write(remoteEP, protocol, request, null); _log.Write(remoteEP, protocol, ex); } finally { if (sendBuffer is not null) ArrayPool.Shared.Return(sendBuffer); } } private async Task AcceptConnectionAsync(Socket tcpListener, DnsTransportProtocol protocol) { IPEndPoint localEP = tcpListener.LocalEndPoint as IPEndPoint; try { tcpListener.SendTimeout = _tcpSendTimeout; tcpListener.ReceiveTimeout = _tcpReceiveTimeout; tcpListener.NoDelay = true; while (true) { Socket socket = await tcpListener.AcceptAsync(); _ = ProcessConnectionAsync(socket, protocol); } } catch (SocketException ex) { if (ex.SocketErrorCode == SocketError.OperationAborted) return; //server stopping _log.Write(localEP, protocol, ex); } catch (ObjectDisposedException) { //server stopped } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(localEP, protocol, ex); } } private async Task ProcessConnectionAsync(Socket socket, DnsTransportProtocol protocol) { IPEndPoint remoteEP = null; try { remoteEP = socket.RemoteEndPoint as IPEndPoint; switch (protocol) { case DnsTransportProtocol.Tcp: await ReadStreamRequestAsync(new NetworkStream(socket), remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol); break; case DnsTransportProtocol.Tls: SslStream tlsStream = new SslStream(new NetworkStream(socket)); string serverName = null; await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return tlsStream.AuthenticateAsServerAsync(delegate (SslStream stream, SslClientHelloInfo clientHelloInfo, object state, CancellationToken cancellationToken) { serverName = clientHelloInfo.ServerName; return ValueTask.FromResult(_dotSslServerAuthenticationOptions); }, null, cancellationToken1); }, _tcpReceiveTimeout); NameServerAddress dnsEP; if (string.IsNullOrEmpty(serverName)) dnsEP = new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tls); else dnsEP = new NameServerAddress(serverName, socket.LocalEndPoint as IPEndPoint, DnsTransportProtocol.Tls); await ReadStreamRequestAsync(tlsStream, remoteEP, dnsEP, protocol); break; case DnsTransportProtocol.TcpProxy: if (!NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL)) { //this feature is intended to be used with a reverse proxy or load balancer on private network return; } ProxyProtocolStream proxyStream = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return ProxyProtocolStream.CreateAsServerAsync(new NetworkStream(socket), cancellationToken1); }, _tcpReceiveTimeout); remoteEP = new IPEndPoint(proxyStream.SourceAddress, proxyStream.SourcePort); await ReadStreamRequestAsync(proxyStream, remoteEP, new NameServerAddress(socket.LocalEndPoint, DnsTransportProtocol.Tcp), protocol); break; default: throw new InvalidOperationException(); } } catch (AuthenticationException) { //ignore TLS auth exception } catch (TimeoutException) { //ignore timeout exception on TLS auth } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } finally { socket.Dispose(); } } private async Task ReadStreamRequestAsync(Stream stream, IPEndPoint remoteEP, NameServerAddress dnsEP, DnsTransportProtocol protocol) { try { using MemoryStream readBuffer = new MemoryStream(64); using MemoryStream writeBuffer = new MemoryStream(2048); using SemaphoreSlim writeSemaphore = new SemaphoreSlim(1, 1); while (true) { if (HasQpmLimitExceeded(remoteEP.Address, DnsTransportProtocol.Tcp)) { _statsManager.QueueUpdate(null, remoteEP, protocol, null, true); break; } DnsDatagram request; //read dns datagram with timeout using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { Task task = DnsDatagram.ReadFromTcpAsync(stream, readBuffer, cancellationTokenSource.Token); if ((await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) && (task.Status != TaskStatus.RanToCompletion)) { //read timed out await stream.DisposeAsync(); return; } cancellationTokenSource.Cancel(); //cancel delay task request = await task; request.SetMetadata(dnsEP); } //process request async _ = ProcessStreamRequestAsync(stream, writeBuffer, writeSemaphore, remoteEP, request, protocol); } } catch (ObjectDisposedException) { //ignore } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } private async Task ProcessStreamRequestAsync(Stream stream, MemoryStream writeBuffer, SemaphoreSlim writeSemaphore, IPEndPoint remoteEP, DnsDatagram request, DnsTransportProtocol protocol) { try { DnsDatagram response = await ProcessRequestAsync(request, remoteEP, protocol, IsRecursionAllowed(remoteEP.Address)); if (response is null) { await stream.DisposeAsync(); _statsManager.QueueUpdate(null, remoteEP, protocol, null, false); return; //drop request } //send response await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1) { await writeSemaphore.WaitAsync(cancellationToken1); try { //send dns datagram await response.WriteToTcpAsync(stream, writeBuffer, cancellationToken1); await stream.FlushAsync(cancellationToken1); } finally { writeSemaphore.Release(); } }, _tcpSendTimeout); _queryLog?.Write(remoteEP, protocol, request, response); _statsManager.QueueUpdate(request, remoteEP, protocol, response, false); } catch (ObjectDisposedException) { //ignore } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { if (request is not null) _queryLog?.Write(remoteEP, protocol, request, null); _log.Write(remoteEP, protocol, ex); } } private async Task AcceptQuicConnectionAsync(QuicListener quicListener) { try { while (true) { try { QuicConnection quicConnection = await quicListener.AcceptConnectionAsync(); _ = ProcessQuicConnectionAsync(quicConnection); } catch (AuthenticationException) { //ignore failed connection handshake } catch (QuicException ex) { if (ex.InnerException is OperationCanceledException) continue; throw; } } } catch (ObjectDisposedException) { //server stopped } catch (Exception ex) { if ((_state == ServiceState.Stopping) || (_state == ServiceState.Stopped)) return; //server stopping _log.Write(quicListener.LocalEndPoint, DnsTransportProtocol.Quic, ex); } } private async Task ProcessQuicConnectionAsync(QuicConnection quicConnection) { try { NameServerAddress dnsEP; if (string.IsNullOrEmpty(quicConnection.TargetHostName)) dnsEP = new NameServerAddress(quicConnection.LocalEndPoint, DnsTransportProtocol.Quic); else dnsEP = new NameServerAddress(quicConnection.TargetHostName, quicConnection.LocalEndPoint, DnsTransportProtocol.Quic); while (true) { if (HasQpmLimitExceeded(quicConnection.RemoteEndPoint.Address, DnsTransportProtocol.Tcp)) { _statsManager.QueueUpdate(null, quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, null, true); break; } QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync(); _ = ProcessQuicStreamRequestAsync(quicStream, quicConnection.RemoteEndPoint, dnsEP); } } catch (QuicException ex) { switch (ex.QuicError) { case QuicError.ConnectionIdle: case QuicError.ConnectionAborted: case QuicError.ConnectionTimeout: break; default: _log.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex); break; } } catch (Exception ex) { _log.Write(quicConnection.RemoteEndPoint, DnsTransportProtocol.Quic, ex); } finally { await quicConnection.DisposeAsync(); } } private async Task ProcessQuicStreamRequestAsync(QuicStream quicStream, IPEndPoint remoteEP, NameServerAddress dnsEP) { MemoryStream sharedBuffer = new MemoryStream(512); DnsDatagram request = null; try { //read dns datagram with timeout using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { Task task = DnsDatagram.ReadFromTcpAsync(quicStream, sharedBuffer, cancellationTokenSource.Token); if ((await Task.WhenAny(task, Task.Delay(_tcpReceiveTimeout, cancellationTokenSource.Token)) != task) && (task.Status != TaskStatus.RanToCompletion)) { //read timed out quicStream.Abort(QuicAbortDirection.Both, (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR); return; } cancellationTokenSource.Cancel(); //cancel delay task request = await task; request.SetMetadata(dnsEP); } //process request async DnsDatagram response = await ProcessRequestAsync(request, remoteEP, DnsTransportProtocol.Quic, IsRecursionAllowed(remoteEP.Address)); if (response is null) { _statsManager.QueueUpdate(null, remoteEP, DnsTransportProtocol.Quic, null, false); return; //drop request } //send response await response.WriteToTcpAsync(quicStream, sharedBuffer); _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, response); _statsManager.QueueUpdate(request, remoteEP, DnsTransportProtocol.Quic, response, false); } catch (IOException) { //ignore QuicException / IOException } catch (Exception ex) { if (request is not null) _queryLog?.Write(remoteEP, DnsTransportProtocol.Quic, request, null); _log.Write(remoteEP, DnsTransportProtocol.Quic, ex); } finally { await sharedBuffer.DisposeAsync(); await quicStream.DisposeAsync(); } } private async Task ProcessDoHRequestAsync(HttpContext context) { IPEndPoint remoteEP = context.GetRemoteEndPoint(); //get the socket connection remote EP DnsDatagram dnsRequest = null; try { HttpRequest request = context.Request; HttpResponse response = context.Response; if (NetworkAccessControl.IsAddressAllowed(remoteEP.Address, _reverseProxyNetworkACL)) { //try to get client's actual IP from X-Real-IP header, if any if (!string.IsNullOrEmpty(_dnsOverHttpRealIpHeader)) { string xRealIp = context.Request.Headers[_dnsOverHttpRealIpHeader]; if (IPAddress.TryParse(xRealIp, out IPAddress address)) remoteEP = new IPEndPoint(address, 0); } } else { if (!request.IsHttps) { //DNS-over-HTTP insecure protocol is intended to be used with an SSL terminated reverse proxy like nginx on private network response.StatusCode = 403; await response.WriteAsync("DNS-over-HTTPS (DoH) queries are supported only on HTTPS."); return; } } if (HasQpmLimitExceeded(remoteEP.Address, DnsTransportProtocol.Tcp)) { _statsManager.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, true); response.StatusCode = 429; await response.WriteAsync("Too Many Requests"); return; } switch (request.Method) { case "GET": bool acceptsDoH = false; string requestAccept = request.Headers.Accept; if (string.IsNullOrEmpty(requestAccept)) { acceptsDoH = true; } else { foreach (string mediaType in requestAccept.Split(',')) { if (mediaType.Equals("application/dns-message", StringComparison.OrdinalIgnoreCase)) { acceptsDoH = true; break; } } } if (!acceptsDoH) { response.Redirect((request.IsHttps ? "https://" : "http://") + request.Headers.Host); return; } string dnsRequestBase64Url = request.Query["dns"]; if (string.IsNullOrEmpty(dnsRequestBase64Url)) { response.StatusCode = 400; await response.WriteAsync("Bad Request"); return; } //convert from base64url to base64 dnsRequestBase64Url = dnsRequestBase64Url.Replace('-', '+'); dnsRequestBase64Url = dnsRequestBase64Url.Replace('_', '/'); //add padding int x = dnsRequestBase64Url.Length % 4; if (x > 0) dnsRequestBase64Url = dnsRequestBase64Url.PadRight(dnsRequestBase64Url.Length - x + 4, '='); using (MemoryStream mS = new MemoryStream(Convert.FromBase64String(dnsRequestBase64Url))) { dnsRequest = DnsDatagram.ReadFrom(mS); dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress())); } break; case "POST": if (!string.Equals(request.Headers.ContentType, "application/dns-message", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = 415; await response.WriteAsync("Unsupported Media Type"); return; } using (MemoryStream mS = new MemoryStream(32)) { await request.Body.CopyToAsync(mS, 32); mS.Position = 0; dnsRequest = DnsDatagram.ReadFrom(mS); dnsRequest.SetMetadata(new NameServerAddress(new Uri(context.Request.GetDisplayUrl()), context.GetLocalIpAddress())); } break; default: throw new InvalidOperationException(); } DnsDatagram dnsResponse = await ProcessRequestAsync(dnsRequest, remoteEP, DnsTransportProtocol.Https, IsRecursionAllowed(remoteEP.Address)); if (dnsResponse is null) { //drop request context.Connection.RequestClose(); _statsManager.QueueUpdate(null, remoteEP, DnsTransportProtocol.Https, null, false); return; } using (MemoryStream mS = new MemoryStream(512)) { dnsResponse.WriteTo(mS); mS.Position = 0; response.ContentType = "application/dns-message"; response.ContentLength = mS.Length; await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1) { await using (Stream s = response.Body) { await mS.CopyToAsync(s, 512, cancellationToken1); } }, _tcpSendTimeout); } _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, dnsResponse); _statsManager.QueueUpdate(dnsRequest, remoteEP, DnsTransportProtocol.Https, dnsResponse, false); } catch (IOException) { //ignore IO exceptions } catch (Exception ex) { if (dnsRequest is not null) _queryLog?.Write(remoteEP, DnsTransportProtocol.Https, dnsRequest, null); _log.Write(remoteEP, DnsTransportProtocol.Https, ex); } } private bool IsRecursionAllowed(IPAddress remoteIP) { switch (_recursion) { case DnsServerRecursion.Allow: return true; case DnsServerRecursion.AllowOnlyForPrivateNetworks: switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: case AddressFamily.InterNetworkV6: return NetUtilities.IsPrivateIP(remoteIP); default: return false; } case DnsServerRecursion.UseSpecifiedNetworkACL: return NetworkAccessControl.IsAddressAllowed(remoteIP, _recursionNetworkACL, true); default: return false; } } private async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) { foreach (IDnsRequestController requestController in _dnsApplicationManager.DnsRequestControllers) { try { DnsRequestControllerAction action = await requestController.GetRequestActionAsync(request, remoteEP, protocol); switch (action) { case DnsRequestControllerAction.DropSilently: return null; //drop request case DnsRequestControllerAction.DropWithRefused: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; //drop request with refused } } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } if (request.ParsingException is not null) { //format error if (request.ParsingException is not IOException) _log.Write(remoteEP, protocol, request.ParsingException); //format error response return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question, null, null, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; } if (request.IsSigned) { if (!request.VerifySignedRequest(_tsigKeys, out DnsDatagram unsignedRequest, out DnsDatagram errorResponse)) { _log.Write(remoteEP, protocol, "DNS Server received a request that failed TSIG signature verification (RCODE: " + errorResponse.RCODE + "; TSIG Error: " + errorResponse.TsigError + ")"); errorResponse.Tag = DnsServerResponseType.Authoritative; return errorResponse; } DnsDatagram unsignedResponse = await ProcessQueryAsync(unsignedRequest, remoteEP, protocol, isRecursionAllowed, false, _clientTimeout, request.TsigKeyName); if (unsignedResponse is null) return null; unsignedResponse = await PostProcessQueryAsync(request, remoteEP, protocol, unsignedResponse); if (unsignedResponse is null) return null; return unsignedResponse.SignResponse(request, _tsigKeys); } if (request.EDNS is not null) { if (request.EDNS.Version != 0) return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.BADVERS, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None) { Tag = DnsServerResponseType.Authoritative }; } DnsDatagram response = await ProcessQueryAsync(request, remoteEP, protocol, isRecursionAllowed, false, _clientTimeout, null); if (response is null) return null; return await PostProcessQueryAsync(request, remoteEP, protocol, response); } private async Task PostProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) { foreach (IDnsPostProcessor postProcessor in _dnsApplicationManager.DnsPostProcessors) { try { response = await postProcessor.PostProcessAsync(request, remoteEP, protocol, response); if (response is null) return null; //drop request } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } if (request.EDNS is null) { if (response.EDNS is not null) response = response.CloneWithoutEDns(); return response; } if (response.EDNS is not null) return response; IReadOnlyList options = null; EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true); if (requestECS is not null) options = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, 0, requestECS.Address); if (response.Additional.Count == 0) return response.Clone(null, null, new DnsResourceRecord[] { DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options) }); if (response.IsSigned) return response; DnsResourceRecord[] newAdditional = new DnsResourceRecord[response.Additional.Count + 1]; for (int i = 0; i < response.Additional.Count; i++) newAdditional[i] = response.Additional[i]; newAdditional[response.Additional.Count] = DnsDatagramEdns.GetOPTFor(_udpPayloadSize, response.RCODE, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); return response.Clone(null, null, newAdditional); } private async Task ProcessQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout, string tsigAuthenticatedKeyName) { if (request.IsResponse) return null; //drop response datagram to avoid loops in rare scenarios switch (request.OPCODE) { case DnsOpcode.StandardQuery: if (request.Question.Count != 1) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (request.Question[0].Class != DnsClass.IN) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; try { DnsQuestionRecord question = request.Question[0]; switch (question.Type) { case DnsResourceRecordType.AXFR: if (protocol == DnsTransportProtocol.Udp) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName); case DnsResourceRecordType.IXFR: return await ProcessZoneTransferQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName); case DnsResourceRecordType.FWD: case DnsResourceRecordType.APP: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } //query authoritative zone DnsDatagram response = await ProcessAuthoritativeQueryAsync(request, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers); if (response is not null) { if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, response.RCODE, request.Question) { Tag = DnsServerResponseType.Authoritative }; return response; } if (!request.RecursionDesired || !isRecursionAllowed) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; //do recursive query if ((question.Type == DnsResourceRecordType.ANY) && (protocol == DnsTransportProtocol.Udp)) //force TCP for ANY request return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, true, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, null, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); } catch (InvalidDomainNameException) { //format error response return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } catch (TimeoutException ex) { DnsDatagram response = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative }; _log.Write(remoteEP, protocol, request, response); _log.Write(remoteEP, protocol, ex); return response; } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question) { Tag = DnsServerResponseType.Authoritative }; } case DnsOpcode.Notify: return await ProcessNotifyQueryAsync(request, remoteEP, protocol); case DnsOpcode.Update: return await ProcessUpdateQueryAsync(request, remoteEP, protocol, tsigAuthenticatedKeyName); default: return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.NotImplemented, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessNotifyQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((zoneInfo is null) || ((zoneInfo.Type != AuthZoneType.Secondary) && (zoneInfo.Type != AuthZoneType.SecondaryForwarder) && (zoneInfo.Type != AuthZoneType.SecondaryCatalog)) || zoneInfo.Disabled) return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; async Task RemoteVerifiedAsync(IPAddress remoteAddress) { if (_notifyAllowedNetworks is not null) { foreach (NetworkAddress notifyAllowedNetwork in _notifyAllowedNetworks) { if (notifyAllowedNetwork.Contains(remoteAddress)) return true; } } IReadOnlyList primaryNameServerAddresses; SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone; if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers) primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses); else primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync(); foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } if (!await RemoteVerifiedAsync(remoteEP.Address)) { _log.Write(remoteEP, protocol, "DNS Server refused a NOTIFY request since the request IP address was not recognized by the secondary zone: " + zoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } _log.Write(remoteEP, protocol, "DNS Server received a NOTIFY request for secondary zone: " + zoneInfo.DisplayName); if ((request.Answer.Count > 0) && (request.Answer[0].Type == DnsResourceRecordType.SOA)) { IReadOnlyList localSoaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (!DnsSOARecordData.IsZoneUpdateAvailable((localSoaRecords[0].RDATA as DnsSOARecordData).Serial, (request.Answer[0].RDATA as DnsSOARecordData).Serial)) { //no update was available return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } zoneInfo.TriggerRefresh(); return new DnsDatagram(request.Identifier, true, DnsOpcode.Notify, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } private async Task ProcessUpdateQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName) { if ((request.Question.Count != 1) || (request.Question[0].Type != DnsResourceRecordType.SOA)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (request.Question[0].Class != DnsClass.IN) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name); if ((zoneInfo is null) || zoneInfo.Disabled) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; _log.Write(remoteEP, protocol, "DNS Server received a zone UPDATE request for zone: " + zoneInfo.DisplayName); async Task IsZoneNameServerAllowedAsync() { IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } async Task IsUpdatePermittedAsync() { bool isUpdateAllowed; switch (zoneInfo.Update) { case AuthZoneUpdate.Allow: isUpdateAllowed = true; break; case AuthZoneUpdate.AllowOnlyZoneNameServers: isUpdateAllowed = await IsZoneNameServerAllowedAsync(); break; case AuthZoneUpdate.UseSpecifiedNetworkACL: isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL); break; case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: isUpdateAllowed = NetworkAccessControl.IsAddressAllowed(remoteEP.Address, zoneInfo.UpdateNetworkACL) || await IsZoneNameServerAllowedAsync(); break; case AuthZoneUpdate.Deny: default: isUpdateAllowed = false; break; } if (!isUpdateAllowed) { _log.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request IP address is not allowed by the zone: " + zoneInfo.DisplayName); return false; } //check security policies if ((zoneInfo.UpdateSecurityPolicies is not null) && (zoneInfo.UpdateSecurityPolicies.Count > 0)) { if ((tsigAuthenticatedKeyName is null) || !zoneInfo.UpdateSecurityPolicies.TryGetValue(tsigAuthenticatedKeyName.ToLowerInvariant(), out IReadOnlyDictionary> policyMap)) { _log.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request since the request is missing TSIG auth required by the zone: " + zoneInfo.DisplayName); return false; } //check policy foreach (DnsResourceRecord uRecord in request.Authority) { bool isPermitted = false; foreach (KeyValuePair> policy in policyMap) { if ( uRecord.Name.Equals(policy.Key, StringComparison.OrdinalIgnoreCase) || (policy.Key.StartsWith("*.") && uRecord.Name.EndsWith(policy.Key.Substring(1), StringComparison.OrdinalIgnoreCase)) ) { foreach (DnsResourceRecordType allowedType in policy.Value) { if ((allowedType == DnsResourceRecordType.ANY) || (allowedType == uRecord.Type)) { isPermitted = true; break; } } if (isPermitted) break; } } if (!isPermitted) { _log.Write(remoteEP, protocol, "DNS Server refused a zone UPDATE request [" + uRecord.Name.ToLowerInvariant() + " " + uRecord.Type.ToString() + " " + uRecord.Class.ToString() + "] due to Dynamic Updates Security Policy for zone: " + zoneInfo.DisplayName); return false; } } } return true; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: //update { //process prerequisite section { Dictionary>> temp = new Dictionary>>(); foreach (DnsResourceRecord prRecord in request.Answer) { if (prRecord.TTL != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(prRecord.Name); if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (prRecord.Class == DnsClass.ANY) { if (prRecord.RDATA.RDLENGTH != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (prRecord.Type == DnsResourceRecordType.ANY) { //check if name is in use if (!_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else { //check if RRSet exists (value independent) IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type); if (rrset.Count == 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (prRecord.Class == DnsClass.NONE) { if (prRecord.RDATA.RDLENGTH != 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (prRecord.Type == DnsResourceRecordType.ANY) { //check if name is not in use if (_authZoneManager.NameExists(zoneInfo.Name, prRecord.Name)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXDomain, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else { //check if RRSet does not exists IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, prRecord.Name, prRecord.Type); if (rrset.Count > 0) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.YXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (prRecord.Class == request.Question[0].Class) { //check if RRSet exists (value dependent) //add to temp for later comparison string recordName = prRecord.Name.ToLowerInvariant(); if (!temp.TryGetValue(recordName, out Dictionary> rrsetEntry)) { rrsetEntry = new Dictionary>(); temp.Add(recordName, rrsetEntry); } if (!rrsetEntry.TryGetValue(prRecord.Type, out List rrset)) { rrset = new List(); rrsetEntry.Add(prRecord.Type, rrset); } rrset.Add(prRecord); } else { //FORMERR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } //compare collected RRSets in temp foreach (KeyValuePair>> zoneEntry in temp) { foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { List prRRSet = rrsetEntry.Value; IReadOnlyList rrset = _authZoneManager.GetRecords(zoneInfo.Name, zoneEntry.Key, rrsetEntry.Key); //check if RRSet exists (value dependent) //compare RRSets if (prRRSet.Count != rrset.Count) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; foreach (DnsResourceRecord prRecord in prRRSet) { bool found = false; foreach (DnsResourceRecord record in rrset) { if ( prRecord.Name.Equals(record.Name, StringComparison.OrdinalIgnoreCase) && (prRecord.Class == record.Class) && (prRecord.Type == record.Type) && (prRecord.RDATA.RDLENGTH == record.RDATA.RDLENGTH) && prRecord.RDATA.Equals(record.RDATA) ) { found = true; break; } } if (!found) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NXRRSet, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } } } //check for permissions if (!await IsUpdatePermittedAsync()) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; //process update section { //prescan foreach (DnsResourceRecord uRecord in request.Authority) { AuthZoneInfo prAuthZoneInfo = _authZoneManager.FindAuthZoneInfo(uRecord.Name); if ((prAuthZoneInfo is null) || !prAuthZoneInfo.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotZone, request.Question) { Tag = DnsServerResponseType.Authoritative }; if (uRecord.Class == request.Question[0].Class) { if (uRecord.RDATA.RDLENGTH == 0) //RDATA must be present to add record return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) { case DnsResourceRecordType.ANY: case DnsResourceRecordType.AXFR: case DnsResourceRecordType.MAILA: case DnsResourceRecordType.MAILB: case DnsResourceRecordType.IXFR: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (uRecord.Class == DnsClass.ANY) { if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH != 0)) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) { case DnsResourceRecordType.AXFR: case DnsResourceRecordType.MAILA: case DnsResourceRecordType.MAILB: case DnsResourceRecordType.IXFR: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else if (uRecord.Class == DnsClass.NONE) { if ((uRecord.TTL != 0) || (uRecord.RDATA.RDLENGTH == 0)) //RDATA must be present for deletion return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (uRecord.Type) { case DnsResourceRecordType.ANY: case DnsResourceRecordType.AXFR: case DnsResourceRecordType.MAILA: case DnsResourceRecordType.MAILB: case DnsResourceRecordType.IXFR: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } else { //FORMERR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } //update Dictionary>> originalRRSets = new Dictionary>>(); void AddToOriginalRRSets(string domain, DnsResourceRecordType type, IReadOnlyList existingRRSet) { if (!originalRRSets.TryGetValue(domain, out Dictionary> originalRRSetEntries)) { originalRRSetEntries = new Dictionary>(); originalRRSets.Add(domain, originalRRSetEntries); } originalRRSetEntries.TryAdd(type, existingRRSet); } try { foreach (DnsResourceRecord uRecord in request.Authority) { if (uRecord.Class == request.Question[0].Class) { //Add to an RRset if (uRecord.Type == DnsResourceRecordType.CNAME) { if (_authZoneManager.NameExists(zoneInfo.Name, uRecord.Name) && (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count == 0)) continue; //current name exists and has non-CNAME records so cannot add CNAME record IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.Comments = "Via Dynamic Updates (RFC 2136)" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? "" : " using key '" + tsigAuthenticatedKeyName + "'") + " from '" + remoteEP.ToString() + "'"; _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.DNAME) { IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.Comments = "Via Dynamic Updates (RFC 2136)" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? "" : " using key '" + tsigAuthenticatedKeyName + "'") + " from '" + remoteEP.ToString() + "'"; _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else if (uRecord.Type == DnsResourceRecordType.SOA) { if (!uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //can add SOA only to apex IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.Comments = "Via Dynamic Updates (RFC 2136)" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? "" : " using key '" + tsigAuthenticatedKeyName + "'") + " from '" + remoteEP.ToString() + "'"; _authZoneManager.SetRecord(zoneInfo.Name, uRecord); } else { if (_authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, DnsResourceRecordType.CNAME).Count > 0) continue; //current name contains CNAME so cannot add non-CNAME record IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); if (uRecord.Type == DnsResourceRecordType.NS) uRecord.SyncGlueRecords(request.Additional); GenericRecordInfo recordInfo = uRecord.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.Comments = "Via Dynamic Updates (RFC 2136)" + (string.IsNullOrEmpty(tsigAuthenticatedKeyName) ? "" : " using key '" + tsigAuthenticatedKeyName + "'") + " from '" + remoteEP.ToString() + "'"; _authZoneManager.AddRecord(zoneInfo.Name, uRecord); } } else if (uRecord.Class == DnsClass.ANY) { if (uRecord.Type == DnsResourceRecordType.ANY) { //Delete all RRsets from a name IReadOnlyDictionary> existingRRSets = _authZoneManager.GetEntriesFor(zoneInfo.Name, uRecord.Name); if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> existingRRSet in existingRRSets) { switch (existingRRSet.Key) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets } AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value); _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key); } } else { foreach (KeyValuePair> existingRRSet in existingRRSets) { switch (existingRRSet.Key) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //skip DNSSEC rrsets } AddToOriginalRRSets(uRecord.Name, existingRRSet.Key, existingRRSet.Value); _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, existingRRSet.Key); } } } else { //Delete an RRset if (uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) { switch (uRecord.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //no apex SOA/NS can be deleted; skip DNSSEC rrsets } } IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.DeleteRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); } } else if (uRecord.Class == DnsClass.NONE) { //Delete an RR from an RRset switch (uRecord.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: continue; //no SOA can be deleted; skip DNSSEC rrsets } IReadOnlyList existingRRSet = _authZoneManager.GetRecords(zoneInfo.Name, uRecord.Name, uRecord.Type); if ((uRecord.Type == DnsResourceRecordType.NS) && (existingRRSet.Count == 1) && uRecord.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) continue; //no apex NS can be deleted if only 1 NS exists AddToOriginalRRSets(uRecord.Name, uRecord.Type, existingRRSet); _authZoneManager.DeleteRecord(zoneInfo.Name, uRecord.Name, uRecord.Type, uRecord.RDATA); } } } catch { //revert foreach (KeyValuePair>> originalRRSetEntries in originalRRSets) { foreach (KeyValuePair> originalRRSet in originalRRSetEntries.Value) { if (originalRRSet.Value.Count == 0) _authZoneManager.DeleteRecords(zoneInfo.Name, originalRRSetEntries.Key, originalRRSet.Key); else _authZoneManager.SetRecords(zoneInfo.Name, originalRRSet.Value); } } throw; } } _authZoneManager.SaveZoneFile(zoneInfo.Name); _log.Write(remoteEP, protocol, "DNS Server successfully processed a zone UPDATE request for zone: " + zoneInfo.DisplayName); //NOERROR return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: //forward { //check for permissions if (!await IsUpdatePermittedAsync()) return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; //forward to primary IReadOnlyList primaryNameServerAddresses; DnsTransportProtocol primaryZoneTransferProtocol; SecondaryCatalogZone secondaryCatalogZone = zoneInfo.ApexZone.SecondaryCatalogZone; if ((secondaryCatalogZone is not null) && !zoneInfo.OverrideCatalogPrimaryNameServers) { primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses); primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol; } else { primaryNameServerAddresses = await zoneInfo.ApexZone.GetResolvedPrimaryNameServerAddressesAsync(); primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol; } switch (primaryZoneTransferProtocol) { case DnsTransportProtocol.Tls: case DnsTransportProtocol.Quic: { //change name server protocol to TLS/QUIC List updatedNameServers = new List(primaryNameServerAddresses.Count); foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.Protocol == primaryZoneTransferProtocol) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.Clone(primaryZoneTransferProtocol)); } primaryNameServerAddresses = updatedNameServers; } break; default: if (protocol == DnsTransportProtocol.Tcp) { //change name server protocol to TCP List updatedNameServers = new List(primaryNameServerAddresses.Count); foreach (NameServerAddress primaryNameServer in primaryNameServerAddresses) { if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.Clone(DnsTransportProtocol.Tcp)); } primaryNameServerAddresses = updatedNameServers; } break; } TsigKey key = null; if (!string.IsNullOrEmpty(tsigAuthenticatedKeyName) && ((_tsigKeys is null) || !_tsigKeys.TryGetValue(tsigAuthenticatedKeyName, out key))) throw new DnsServerException("DNS Server does not have TSIG key '" + tsigAuthenticatedKeyName + "' configured to authenticate dynamic updates for " + zoneInfo.TypeName + " zone: " + zoneInfo.DisplayName); DnsClient dnsClient = new DnsClient(primaryNameServerAddresses); dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = 1; DnsDatagram newRequest = request.Clone(); newRequest.SetRandomIdentifier(); DnsDatagram newResponse; if (key is null) newResponse = await dnsClient.RawResolveAsync(newRequest); else newResponse = await dnsClient.TsigResolveAsync(newRequest, key); newResponse.SetIdentifier(request.Identifier); return newResponse; } default: return new DnsDatagram(request.Identifier, true, DnsOpcode.Update, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotAuth, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessZoneTransferQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, string tsigAuthenticatedKeyName) { AuthZoneInfo zoneInfo = _authZoneManager.GetAuthZoneInfo(request.Question[0].Name); if ((zoneInfo is null) || !zoneInfo.ApexZone.IsActive) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: break; default: return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } async Task IsZoneNameServerAllowedAsync(ApexZone apexZone) { IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await apexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) return true; } return false; } async Task IsZoneTransferAllowed(ApexZone apexZone) { switch (apexZone.ZoneTransfer) { case AuthZoneTransfer.Allow: return true; case AuthZoneTransfer.AllowOnlyZoneNameServers: return await IsZoneNameServerAllowedAsync(apexZone); case AuthZoneTransfer.UseSpecifiedNetworkACL: return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL); case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: return NetworkAccessControl.IsAddressAllowed(remoteEP.Address, apexZone.ZoneTransferNetworkACL) || await IsZoneNameServerAllowedAsync(apexZone); case AuthZoneTransfer.Deny: default: return false; } } bool IsTsigAuthenticated(ApexZone apexZone) { if ((apexZone.ZoneTransferTsigKeyNames is null) || (apexZone.ZoneTransferTsigKeyNames.Count < 1)) return true; //no auth needed if ((tsigAuthenticatedKeyName is not null) && apexZone.ZoneTransferTsigKeyNames.Contains(tsigAuthenticatedKeyName.ToLowerInvariant())) return true; //key matches return false; } bool isInZoneTransferAllowedList = false; if (_zoneTransferAllowedNetworks is not null) { IPAddress remoteAddress = remoteEP.Address; foreach (NetworkAddress networkAddress in _zoneTransferAllowedNetworks) { if (networkAddress.Contains(remoteAddress)) { isInZoneTransferAllowedList = true; break; } } } if (!isInZoneTransferAllowedList) { ApexZone apexZone = zoneInfo.ApexZone; CatalogZone catalogZone = apexZone.CatalogZone; if (catalogZone is not null) { if (!apexZone.OverrideCatalogZoneTransfer) apexZone = catalogZone; //use catalog zone transfer options } else { SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone; if (secondaryCatalogZone is not null) { if (!apexZone.OverrideCatalogZoneTransfer) apexZone = secondaryCatalogZone; //use secondary zone transfer options } } if (!await IsZoneTransferAllowed(apexZone)) { _log.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request IP address is not allowed by the zone: " + zoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } if (!IsTsigAuthenticated(apexZone)) { _log.Write(remoteEP, protocol, "DNS Server refused a zone transfer request since the request is missing TSIG auth required by the zone: " + zoneInfo.DisplayName); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question) { Tag = DnsServerResponseType.Authoritative }; } } _log.Write(remoteEP, protocol, "DNS Server received zone transfer request for zone: " + zoneInfo.DisplayName); IReadOnlyList xfrRecords; if (request.Question[0].Type == DnsResourceRecordType.IXFR) { if ((request.Authority.Count == 1) && (request.Authority[0].Type == DnsResourceRecordType.SOA)) xfrRecords = _authZoneManager.QueryIncrementalZoneTransferRecords(request.Question[0].Name, request.Authority[0]); else return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else { xfrRecords = _authZoneManager.QueryZoneTransferRecords(request.Question[0].Name); } DnsDatagram xfrResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, xfrRecords) { Tag = DnsServerResponseType.Authoritative }; xfrResponse = xfrResponse.Split(); //update notify failed list NameServerAddress allowedZoneNameServer = null; switch (zoneInfo.Notify) { case AuthZoneNotify.ZoneNameServers: case AuthZoneNotify.BothZoneAndSpecifiedNameServers: IPAddress remoteAddress = remoteEP.Address; IReadOnlyList secondaryNameServers = await zoneInfo.ApexZone.GetResolvedSecondaryNameServerAddressesAsync(); foreach (NameServerAddress secondaryNameServer in secondaryNameServers) { if (secondaryNameServer.IPEndPoint.Address.Equals(remoteAddress)) { allowedZoneNameServer = secondaryNameServer; break; } } break; } zoneInfo.ApexZone.RemoveFromNotifyFailedList(allowedZoneNameServer, remoteEP.Address); return xfrResponse; } private async Task ProcessAuthoritativeQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers) { DnsDatagram response = await AuthoritativeQueryAsync(request, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, remoteEP); if (response is null) return null; bool reprocessResponse; //to allow resolving CNAME/ANAME in response do { reprocessResponse = false; if (response.RCODE == DnsResponseCode.NoError) { if (response.Answer.Count > 0) { DnsResourceRecordType questionType = request.Question[0].Type; DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type != questionType) && (questionType != DnsResourceRecordType.ANY)) { switch (lastRR.Type) { case DnsResourceRecordType.CNAME: return await ProcessCNAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout); case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: return await ProcessANAMEAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout); } } } else if (response.Authority.Count > 0) { DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: if (request.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution (with blocking support) using empty conditional forwarders; name servers will be provided via ResolverDnsCache return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, [], _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout); } break; case DnsResourceRecordType.FWD: //do conditional forwarding (with blocking support) return await ProcessRecursiveQueryAsync(request, remoteEP, protocol, response.Authority, _dnssecValidation, false, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout); case DnsResourceRecordType.APP: response = await ProcessAPPAsync(request, response, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, _clientTimeout); if (response is null) return null; //drop request reprocessResponse = true; break; } } } } while (reprocessResponse); return response; } internal async Task AuthoritativeQueryAsync(DnsDatagram request, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, IPEndPoint remoteEP = null) { DnsDatagram authResponse; if (remoteEP is null) authResponse = _authZoneManager.Query(request, isRecursionAllowed); else authResponse = await _authZoneManager.QueryAsync(request, remoteEP.Address, isRecursionAllowed); if (authResponse is not null) { if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA()) { authResponse.Tag = DnsServerResponseType.Authoritative; return authResponse; } } DnsDatagram lastAppResponse = null; if (!skipDnsAppAuthoritativeRequestHandlers) { if (remoteEP is null) remoteEP = IPENDPOINT_ANY_0; foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsApplicationManager.DnsAuthoritativeRequestHandlers) { try { DnsDatagram appResponse = await requestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed); if (appResponse is not null) { if ((appResponse.RCODE != DnsResponseCode.NoError) || (appResponse.Answer.Count > 0) || (appResponse.Authority.Count == 0) || appResponse.IsFirstAuthoritySOA()) { if (appResponse.Tag is null) appResponse.Tag = DnsServerResponseType.Authoritative; return appResponse; } if (lastAppResponse is null) { //keep last non-null app response to return later lastAppResponse = appResponse; } else { //compare last app response and current app response and select more specific response DnsResourceRecord appResponseFirstAuthority = appResponse.FindFirstAuthorityRecord(); DnsResourceRecord lastAppResponseFirstAuthority = lastAppResponse.FindFirstAuthorityRecord(); if (appResponseFirstAuthority.Name.Length > lastAppResponseFirstAuthority.Name.Length) lastAppResponse = appResponse; } } } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } } if ((authResponse is not null) && (authResponse.Authority.Count > 0)) { if ((lastAppResponse is not null) && (lastAppResponse.Authority.Count > 0)) { DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord(); DnsResourceRecord appResponseFirstAuthority = lastAppResponse.FindFirstAuthorityRecord(); if (appResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length) return lastAppResponse; } return authResponse; } else { return lastAppResponse; } } private async Task ProcessAPPAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout) { DnsResourceRecord appResourceRecord = response.Authority[0]; DnsApplicationRecordData appRecord = appResourceRecord.RDATA as DnsApplicationRecordData; if (_dnsApplicationManager.Applications.TryGetValue(appRecord.AppName, out DnsApplication application)) { if (application.DnsAppRecordRequestHandlers.TryGetValue(appRecord.ClassPath, out IDnsAppRecordRequestHandler appRecordRequestHandler)) { AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(appResourceRecord.Name); DnsDatagram appResponse = await appRecordRequestHandler.ProcessRequestAsync(request, remoteEP, protocol, isRecursionAllowed, zoneInfo.Name, appResourceRecord.Name, appResourceRecord.TTL, appRecord.Data); if (appResponse is null) { DnsResponseCode rcode; IReadOnlyList authority = null; if ((zoneInfo.Type == AuthZoneType.Forwarder) || (zoneInfo.Type == AuthZoneType.SecondaryForwarder)) { //process FWD record if exists if (!zoneInfo.Name.Equals(appResourceRecord.Name, StringComparison.OrdinalIgnoreCase)) { AuthZone authZone = _authZoneManager.GetAuthZone(zoneInfo.Name, appResourceRecord.Name); if (authZone is not null) authority = authZone.QueryRecords(DnsResourceRecordType.FWD, false); } if ((authority is null) || (authority.Count == 0)) authority = zoneInfo.ApexZone.QueryRecords(DnsResourceRecordType.FWD, false); if (authority.Count > 0) return await RecursiveResolveAsync(request, remoteEP, authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); rcode = DnsResponseCode.NoError; } else { //return NODATA/NXDOMAIN response if ((request.Question[0].Name.Length == appResourceRecord.Name.Length) || appResourceRecord.Name.StartsWith('*')) rcode = DnsResponseCode.NoError; else rcode = DnsResponseCode.NxDomain; authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); } return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; } else { if (appResponse.AuthoritativeAnswer) appResponse.Tag = DnsServerResponseType.Authoritative; return appResponse; //return app response } } else { _log.Write(remoteEP, protocol, "DNS request handler '" + appRecord.ClassPath + "' was not found in the application '" + appRecord.AppName + "': " + appResourceRecord.Name); } } else { _log.Write(remoteEP, protocol, "DNS application '" + appRecord.AppName + "' was not found: " + appResourceRecord.Name); } //return server failure response with SOA { AuthZoneInfo zoneInfo = _authZoneManager.FindAuthZoneInfo(request.Question[0].Name); IReadOnlyList authority = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, authority) { Tag = DnsServerResponseType.Authoritative }; } } private async Task ProcessCNAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout) { List newAnswer = new List(response.Answer.Count + 4); newAnswer.AddRange(response.Answer); //copying NSEC/NSEC3 for for wildcard answers List newAuthority = new List(2); foreach (DnsResourceRecord record in response.Authority) { switch (record.Type) { case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: newAuthority.Add(record); break; case DnsResourceRecordType.RRSIG: switch ((record.RDATA as DnsRRSIGRecordData).TypeCovered) { case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: newAuthority.Add(record); break; } break; } } DnsDatagram lastResponse = response; bool isAuthoritativeAnswer = response.AuthoritativeAnswer; DnsResourceRecord lastRR = response.GetLastAnswerRecord(); EDnsOption[] eDnsClientSubnetOption = null; DnsDatagram newResponse = null; double responseRtt = 0.0; if (response.Metadata is not null) responseRtt = response.Metadata.RoundTripTime; if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) eDnsClientSubnetOption = [new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS)]; } int queryCount = 0; do { string cnameDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain; if (lastRR.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase)) break; //loop detected DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(cnameDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption); //query authoritative zone first newResponse = await AuthoritativeQueryAsync(newRequest, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, remoteEP); if (newResponse is null) { //not found in auth zone if (newRequest.RecursionDesired && isRecursionAllowed) { //do recursion newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); //CNAME expansion does not need to use cache refresh operation and should use data from cache instead if (newResponse is null) return null; //drop request isAuthoritativeAnswer = false; } else { //break since no recursion allowed/desired break; } } else if ((newResponse.Answer.Count > 0) && (newResponse.GetLastAnswerRecord() is DnsResourceRecord lastAnswer) && ((lastAnswer.Type == DnsResourceRecordType.ANAME) || (lastAnswer.Type == DnsResourceRecordType.ALIAS))) { newResponse = await ProcessANAMEAsync(request, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { //found delegated/forwarded zone DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: if (newRequest.RecursionDesired && isRecursionAllowed) { //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolveDnsCache newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request isAuthoritativeAnswer = false; } break; case DnsResourceRecordType.FWD: //do conditional forwarding newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request isAuthoritativeAnswer = false; break; case DnsResourceRecordType.APP: newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request break; } } if (newResponse.Metadata is not null) responseRtt += newResponse.Metadata.RoundTripTime; //check last response if (newResponse.Answer.Count == 0) break; //cannot proceed to resolve further lastRR = newResponse.GetLastAnswerRecord(); if (lastRR.Type != DnsResourceRecordType.CNAME) { newAnswer.AddRange(newResponse.Answer); break; //cname was resolved } bool foundRepeat = false; foreach (DnsResourceRecord newResponseAnswerRecord in newResponse.Answer) { if ((newResponseAnswerRecord.Type == DnsResourceRecordType.CNAME) || (newResponseAnswerRecord.Type == DnsResourceRecordType.DNAME)) { foreach (DnsResourceRecord answerRecord in newAnswer) { if (newResponseAnswerRecord.Equals(answerRecord)) { foundRepeat = true; break; } } if (foundRepeat) break; } newAnswer.Add(newResponseAnswerRecord); } if (foundRepeat) break; //loop detected lastResponse = newResponse; } while (++queryCount < MAX_CNAME_HOPS); DnsResponseCode rcode; IReadOnlyList authority; IReadOnlyList additional; if (newResponse is null) { //no recursion available rcode = DnsResponseCode.NoError; if (newAuthority.Count == 0) { authority = lastResponse.Authority; } else { newAuthority.AddRange(lastResponse.Authority); authority = newAuthority; } additional = lastResponse.Additional; } else { rcode = newResponse.RCODE; if (newAuthority.Count == 0) { authority = newResponse.Authority; } else { newAuthority.AddRange(newResponse.Authority); authority = newAuthority; } additional = newResponse.Additional; } DnsDatagram finalResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, isAuthoritativeAnswer, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, newAnswer, authority, additional) { Tag = response.Tag }; finalResponse.SetMetadata(null, responseRtt); return finalResponse; } private async Task ProcessANAMEAsync(DnsDatagram request, DnsDatagram response, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout) { EDnsOption[] eDnsClientSubnetOption = null; if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) eDnsClientSubnetOption = [new EDnsOption(EDnsOptionCode.EDNS_CLIENT_SUBNET, requestECS)]; } Queue>> resolveQueue = new Queue>>(); async Task> ResolveANAMEAsync(DnsResourceRecord anameRR, int queryCount = 0) { string lastDomain = (anameRR.RDATA as DnsANAMERecordData).Domain; if (anameRR.Name.Equals(lastDomain, StringComparison.OrdinalIgnoreCase)) return null; //loop detected do { DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, request.CheckingDisabled, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(lastDomain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, eDnsClientSubnetOption); //query authoritative zone first DnsDatagram newResponse = await AuthoritativeQueryAsync(newRequest, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, remoteEP); if (newResponse is null) { //not found in auth zone; do recursion newResponse = await RecursiveResolveAsync(newRequest, remoteEP, null, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request } else if ((newResponse.Answer.Count == 0) && (newResponse.Authority.Count > 0)) { //found delegated/forwarded zone DnsResourceRecord firstAuthority = newResponse.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: //do forced recursive resolution using empty conditional forwarders; name servers will be provided via ResolverDnsCache newResponse = await RecursiveResolveAsync(newRequest, remoteEP, [], _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request break; case DnsResourceRecordType.FWD: //do conditional forwarding newResponse = await RecursiveResolveAsync(newRequest, remoteEP, newResponse.Authority, _dnssecValidation, false, false, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request break; case DnsResourceRecordType.APP: newResponse = await ProcessAPPAsync(newRequest, newResponse, remoteEP, protocol, isRecursionAllowed, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (newResponse is null) return null; //drop request break; } } //check new response if (newResponse.RCODE != DnsResponseCode.NoError) return null; //cannot proceed to resolve further if (newResponse.Answer.Count == 0) return Array.Empty(); //NO DATA DnsResourceRecordType questionType = request.Question[0].Type; DnsResourceRecord lastRR = newResponse.GetLastAnswerRecord(); if (lastRR.Type == questionType) { //found final answer List answers = new List(); foreach (DnsResourceRecord answer in newResponse.Answer) { if (answer.Type != questionType) continue; if (anameRR.TTL < answer.TTL) answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, anameRR.TTL, answer.RDATA)); else answers.Add(new DnsResourceRecord(anameRR.Name, answer.Type, answer.Class, answer.TTL, answer.RDATA)); } return answers; } switch (lastRR.Type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: if (newResponse.Answer.Count == 1) { lastDomain = (lastRR.RDATA as DnsANAMERecordData).Domain; } else { //resolve multiple ANAME records async queryCount++; //increment since one query was done already foreach (DnsResourceRecord newAnswer in newResponse.Answer) resolveQueue.Enqueue(ResolveANAMEAsync(newAnswer, queryCount)); return Array.Empty(); } break; case DnsResourceRecordType.CNAME: lastDomain = (lastRR.RDATA as DnsCNAMERecordData).Domain; break; default: //aname/cname was resolved, but no answer found return Array.Empty(); } } while (++queryCount < MAX_CNAME_HOPS); //max hops limit crossed return null; } List responseAnswer = new List(); foreach (DnsResourceRecord answer in response.Answer) { switch (answer.Type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: resolveQueue.Enqueue(ResolveANAMEAsync(answer)); break; default: if (resolveQueue.Count == 0) responseAnswer.Add(answer); break; } } bool foundErrors = false; while (resolveQueue.Count > 0) { IReadOnlyList records = await resolveQueue.Dequeue(); if (records is null) foundErrors = true; else if (records.Count > 0) responseAnswer.AddRange(records); } DnsResponseCode rcode = DnsResponseCode.NoError; IReadOnlyList authority = null; if (responseAnswer.Count == 0) { if (foundErrors) { rcode = DnsResponseCode.ServerFailure; } else { authority = response.Authority; //update last used on DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in authority) record.GetAuthGenericRecordInfo().LastUsedOn = utcNow; } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, request.CheckingDisabled, rcode, request.Question, responseAnswer, authority, null) { Tag = response.Tag }; } private async Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { if (request.Question.Count > 0) { DnsQuestionRecord question = request.Question[0]; if (question.Type == DnsResourceRecordType.DS) { //DS is at parent zone which causes IsAllowed() to return null; change QTYPE to A to fix this issue that causes allowed domains to fail DNSSEC validation at downstream DnsQuestionRecord newQuestion = new DnsQuestionRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN); request = new DnsDatagram(request.Identifier, request.IsResponse, request.OPCODE, request.AuthoritativeAnswer, request.Truncation, request.RecursionDesired, request.RecursionAvailable, request.AuthenticData, request.CheckingDisabled, request.RCODE, [newQuestion], request.Answer, request.Authority, request.Additional); } } if (_enableBlocking) { if (_blockingBypassList is not null) { IPAddress remoteIP = remoteEP.Address; foreach (NetworkAddress network in _blockingBypassList) { if (network.Contains(remoteIP)) return true; } } if (_allowedZoneManager.IsAllowed(request) || _blockListZoneManager.IsAllowed(request)) return true; } foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers) { try { if (await blockingHandler.IsAllowedAsync(request, remoteEP)) return true; } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } return false; } private async Task ProcessBlockedQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol) { if (_enableBlocking) { DnsDatagram response = _blockedZoneManager.Query(request); if (response is null) { //domain not blocked in blocked zone response = _blockListZoneManager.Query(request); //check in block list zone if (response is not null) { //domain is blocked in block list zone response.Tag = DnsServerResponseType.Blocked; return response; } //domain not blocked in block list zone; continue to check app blocking handlers } else { //domain is blocked in blocked zone DnsQuestionRecord question = request.Question[0]; string GetBlockedDomain() { DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); if ((firstAuthority is not null) && (firstAuthority.Type == DnsResourceRecordType.SOA)) return firstAuthority.Name; else return question.Name; } if (_allowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) { //return meta data string blockedDomain = GetBlockedDomain(); IReadOnlyList answer = [new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _blockingAnswerTtl, new DnsTXTRecordData("source=blocked-zone; domain=" + blockedDomain))]; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer) { Tag = DnsServerResponseType.Blocked }; } else { string blockedDomain = null; EDnsOption[] options = null; if (_allowTxtBlockingReport && (request.EDNS is not null)) { blockedDomain = GetBlockedDomain(); options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=blocked-zone; domain=" + blockedDomain))]; } IReadOnlyCollection aRecords; IReadOnlyCollection aaaaRecords; switch (_blockingType) { case DnsServerBlockingType.AnyAddress: aRecords = _aRecords; aaaaRecords = _aaaaRecords; break; case DnsServerBlockingType.CustomAddress: aRecords = _customBlockingARecords; aaaaRecords = _customBlockingAAAARecords; break; case DnsServerBlockingType.NxDomain: if (blockedDomain is null) blockedDomain = GetBlockedDomain(); string parentDomain = AuthZoneManager.GetParentZone(blockedDomain); if (parentDomain is null) parentDomain = string.Empty; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, _blockingAnswerTtl, _blockedZoneManager.DnsSOARecord)], null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; default: throw new InvalidOperationException(); } IReadOnlyList answer; IReadOnlyList authority = null; switch (question.Type) { case DnsResourceRecordType.A: { if (aRecords.Count > 0) { DnsResourceRecord[] rrList = new DnsResourceRecord[aRecords.Count]; int i = 0; foreach (DnsARecordData record in aRecords) rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _blockingAnswerTtl, record); answer = rrList; } else { answer = null; authority = response.Authority; } } break; case DnsResourceRecordType.AAAA: { if (aaaaRecords.Count > 0) { DnsResourceRecord[] rrList = new DnsResourceRecord[aaaaRecords.Count]; int i = 0; foreach (DnsAAAARecordData record in aaaaRecords) rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _blockingAnswerTtl, record); answer = rrList; } else { answer = null; authority = response.Authority; } } break; default: answer = response.Answer; authority = response.Authority; break; } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _udpPayloadSize, EDnsHeaderFlags.None, options) { Tag = DnsServerResponseType.Blocked }; } } } foreach (IDnsRequestBlockingHandler blockingHandler in _dnsApplicationManager.DnsRequestBlockingHandlers) { try { DnsDatagram appBlockedResponse = await blockingHandler.ProcessRequestAsync(request, remoteEP); if (appBlockedResponse is not null) { if (appBlockedResponse.Tag is null) appBlockedResponse.Tag = DnsServerResponseType.Blocked; return appBlockedResponse; } } catch (Exception ex) { _log.Write(remoteEP, protocol, ex); } } return null; } private async Task ProcessRecursiveQueryAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout) { bool isAllowed; if (cacheRefreshOperation) { //cache refresh operation should be able to refresh all the records in cache //this is since a blocked CNAME record could still be used by an allowed domain name and so must resolve isAllowed = true; } else { isAllowed = await IsAllowedAsync(request, remoteEP, protocol); if (!isAllowed) { DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(request, remoteEP, protocol); if (blockedResponse is not null) return blockedResponse; } } DnsDatagram response = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, dnssecValidation, false, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (response is null) return null; //drop request if (response.Answer.Count > 0) { DnsResourceRecordType questionType = request.Question[0].Type; DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type != questionType) && (lastRR.Type == DnsResourceRecordType.CNAME) && (questionType != DnsResourceRecordType.ANY)) { response = await ProcessCNAMEAsync(request, response, remoteEP, protocol, true, skipDnsAppAuthoritativeRequestHandlers, clientTimeout); if (response is null) return null; //drop request } if (!isAllowed) { //check for CNAME cloaking for (int i = 0; i < response.Answer.Count; i++) { DnsResourceRecord record = response.Answer[i]; if (record.Type != DnsResourceRecordType.CNAME) break; //no further CNAME records exists DnsDatagram newRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord((record.RDATA as DnsCNAMERecordData).Domain, request.Question[0].Type, request.Question[0].Class) }, null, null, null, _udpPayloadSize); if (request.Metadata is not null) newRequest.SetMetadata(request.Metadata.NameServer); //check allowed zone isAllowed = await IsAllowedAsync(newRequest, remoteEP, protocol); if (isAllowed) break; //CNAME is in allowed zone //check blocked zone and block list zone DnsDatagram blockedResponse = await ProcessBlockedQueryAsync(newRequest, remoteEP, protocol); if (blockedResponse is not null) { //found cname cloaking List answer = new List(); //copy current and previous CNAME records for (int j = 0; j <= i; j++) answer.Add(response.Answer[j]); //copy last response answers answer.AddRange(blockedResponse.Answer); //include blocked response additional section to pass on Extended DNS Errors return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, false, false, blockedResponse.RCODE, request.Question, answer, blockedResponse.Authority, blockedResponse.Additional) { Tag = blockedResponse.Tag }; } } } } if (response.Tag is null) { if (response.IsBlockedResponse()) response.Tag = DnsServerResponseType.UpstreamBlocked; } else if ((DnsServerResponseType)response.Tag == DnsServerResponseType.Cached) { if (response.IsBlockedResponse()) response.Tag = DnsServerResponseType.UpstreamBlockedCached; } return response; } private async Task RecursiveResolveAsync(DnsDatagram request, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, int clientTimeout) { DnsQuestionRecord question = request.Question[0]; NetworkAddress eDnsClientSubnet = null; bool advancedForwardingClientSubnet = false; //this feature is used by Advanced Forwarding app to cache response per network group if (_eDnsClientSubnet) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is null) { if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork)) { //set ipv4 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv4Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6)) { //set ipv6 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv6Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else if (!NetUtilities.IsPrivateIP(remoteEP.Address)) { //set shadow ECS option switch (remoteEP.AddressFamily) { case AddressFamily.InterNetwork: eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv4PrefixLength); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; case AddressFamily.InterNetworkV6: eDnsClientSubnet = new NetworkAddress(remoteEP.Address, _eDnsClientSubnetIPv6PrefixLength); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; default: request.ShadowHideEDnsClientSubnetOption(); break; } } } else if ((requestECS.Family != EDnsClientSubnetAddressFamily.IPv4) && (requestECS.Family != EDnsClientSubnetAddressFamily.IPv6)) { return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.FormatError, request.Question) { Tag = DnsServerResponseType.Authoritative }; } else if (requestECS.AdvancedForwardingClientSubnet) { //request from Advanced Forwarding app advancedForwardingClientSubnet = true; eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); } else if ((requestECS.SourcePrefixLength == 0) || NetUtilities.IsPrivateIP(requestECS.Address)) { //disable ECS option request.ShadowHideEDnsClientSubnetOption(); } else if ((_eDnsClientSubnetIpv4Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetwork)) { //set ipv4 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv4Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else if ((_eDnsClientSubnetIpv6Override is not null) && (remoteEP.AddressFamily == AddressFamily.InterNetworkV6)) { //set ipv6 override shadow ECS option eDnsClientSubnet = _eDnsClientSubnetIpv6Override; request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); } else { //use ECS from client request switch (requestECS.Family) { case EDnsClientSubnetAddressFamily.IPv4: eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv4PrefixLength)); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; case EDnsClientSubnetAddressFamily.IPv6: eDnsClientSubnet = new NetworkAddress(requestECS.Address, Math.Min(requestECS.SourcePrefixLength, _eDnsClientSubnetIPv6PrefixLength)); request.SetShadowEDnsClientSubnetOption(eDnsClientSubnet); break; } } } else { //ECS feature disabled EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet; if (advancedForwardingClientSubnet) eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); //request from Advanced Forwarding app else request.ShadowHideEDnsClientSubnetOption(); //hide ECS option } } if (!cachePrefetchOperation && !cacheRefreshOperation) { //query cache zone to see if answer available DnsDatagram cacheResponse = await QueryCacheAsync(request, false, false); if (cacheResponse is not null) { if (_cachePrefetchTrigger > 0) { //inspect response TTL values to decide if prefetch trigger is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && ((answer.TTL <= _cachePrefetchTrigger) || answer.IsStale)) { //trigger prefetch async for this specific answer record _ = PrefetchCacheAsync(new DnsQuestionRecord(answer.Name, question.Type, question.Class), remoteEP, conditionalForwarders); break; } } } return cacheResponse; } } //recursion with locking TaskCompletionSource resolverTaskCompletionSource = new TaskCompletionSource(); Task resolverTask = _resolverTasks.GetOrAdd(GetResolverQueryKey(question, eDnsClientSubnet), resolverTaskCompletionSource.Task); if (resolverTask.Equals(resolverTaskCompletionSource.Task)) { //got new resolver task added so question is not being resolved; do recursive resolution in another task on resolver thread pool if (!_resolverTaskPool.TryQueueTask(delegate (object state) { return RecursiveResolverBackgroundTaskAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, conditionalForwarders, dnssecValidation, cachePrefetchOperation, cacheRefreshOperation, skipDnsAppAuthoritativeRequestHandlers, resolverTaskCompletionSource); }) ) { //resolver queue full if (!_resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _)) //remove recursion lock entry throw new InvalidOperationException(); return null; //drop request } } //request is being recursively resolved by another thread if (cachePrefetchOperation) return null; //return null as prefetch worker thread does not need valid response and thus does not need to wait if (_serveStale) { int waitTimeout = Math.Min(_serveStaleMaxWaitTime, clientTimeout - SERVE_STALE_TIME_DIFFERENCE); //200ms before client timeout or max 1800ms [RFC 8767] using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(); //wait till short timeout for response if ((waitTimeout > 0) && ((await Task.WhenAny(resolverTask, Task.Delay(waitTimeout, timeoutCancellationTokenSource.Token)) == resolverTask) || (resolverTask.Status == TaskStatus.RanToCompletion))) { //resolver signaled timeoutCancellationTokenSource.Cancel(); //to stop delay task RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception } else { //wait timed out //query cache zone to return stale answer (if available) as per RFC 8767 DnsDatagram staleResponse = await QueryCacheAsync(request, true, false); if (staleResponse is not null) return staleResponse; //no stale record was found //wait till full timeout before responding as ServerFailure int timeout = clientTimeout - waitTimeout; if ((await Task.WhenAny(resolverTask, Task.Delay(timeout, timeoutCancellationTokenSource.Token)) == resolverTask) || (resolverTask.Status == TaskStatus.RanToCompletion)) { //resolver signaled timeoutCancellationTokenSource.Cancel(); //to stop delay task RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception } } } else { using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(); //wait till full client timeout for response if ((await Task.WhenAny(resolverTask, Task.Delay(clientTimeout, timeoutCancellationTokenSource.Token)) == resolverTask) || (resolverTask.Status == TaskStatus.RanToCompletion)) { //resolver signaled timeoutCancellationTokenSource.Cancel(); //to stop delay task RecursiveResolveResponse response = await resolverTask; if (response is not null) return PrepareRecursiveResolveResponse(request, response); //resolver had exception } } //no response available; respond with ServerFailure EDnsOption[] options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Waiting for resolver. Please try again."))]; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.ServerFailure, request.Question, null, null, null, _udpPayloadSize, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); } private async Task RecursiveResolverBackgroundTaskAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IReadOnlyList conditionalForwarders, bool dnssecValidation, bool cachePrefetchOperation, bool cacheRefreshOperation, bool skipDnsAppAuthoritativeRequestHandlers, TaskCompletionSource taskCompletionSource) { try { //recursive resolve and update cache IDnsCache dnsCache; if (cachePrefetchOperation || cacheRefreshOperation) dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); else if (skipDnsAppAuthoritativeRequestHandlers || advancedForwardingClientSubnet) dnsCache = _dnsCacheSkipDnsApps; //to prevent request reaching apps again else dnsCache = _dnsCache; DnsDatagram response; if (conditionalForwarders is not null) { if (conditionalForwarders.Count > 0) { //do priority based conditional forwarding response = await PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders); } else { //do force recursive resolution response = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, true, cancellationToken: cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT); } } else { //do default recursive resolution response = await DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, dnssecValidation, skipDnsAppAuthoritativeRequestHandlers); } switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: taskCompletionSource.SetResult(new RecursiveResolveResponse(response, response)); break; default: throw new DnsServerException("All name servers failed to answer the request '" + question.ToString() + "'. Received last response with RCODE=" + response.RCODE.ToString() + (response.Metadata is null ? "." : " from: " + response.Metadata.NameServer)); } } catch (Exception ex) { if (_resolverLog is not null) { string strForwarders = null; if (conditionalForwarders is not null) { //empty conditional forwarder array is used to force recursive resolution if (conditionalForwarders.Count > 0) { foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { NameServerAddress nameServer = (conditionalForwarder.RDATA as DnsForwarderRecordData).NameServer; if (strForwarders is null) strForwarders = nameServer.ToString(); else strForwarders += ", " + nameServer.ToString(); } } } else if ((_forwarders is not null) && (_forwarders.Count > 0)) { foreach (NameServerAddress nameServer in _forwarders) { if (strForwarders is null) strForwarders = nameServer.ToString(); else strForwarders += ", " + nameServer.ToString(); } } _resolverLog.Write("DNS Server failed to resolve the request '" + question.ToString() + "'" + (strForwarders is null ? "" : " using forwarders: " + strForwarders) + ".\r\n" + ex.ToString()); } //fetch failure/stale response to signal; reset stale records DnsDatagram cacheRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, dnssecValidation, DnsResponseCode.NoError, [question], null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(eDnsClientSubnet)); DnsDatagram cacheResponse = await QueryCacheAsync(cacheRequest, _serveStale, _serveStale); if (cacheResponse is not null) { //signal failure/stale response if (!dnssecValidation || cacheResponse.AuthenticData) { //no dnssec validation enabled OR cache response is validated data taskCompletionSource.SetResult(new RecursiveResolveResponse(cacheResponse, cacheResponse)); } else { //dnssec validation enabled; cache response may be a bogus/failure response static bool HasBogusRecords(IReadOnlyList records) { foreach (DnsResourceRecord record in records) { switch (record.DnssecStatus) { case DnssecStatus.Disabled: case DnssecStatus.Secure: case DnssecStatus.Insecure: case DnssecStatus.Indeterminate: break; default: return true; } } return false; } bool isFailureResponse = false; switch (cacheResponse.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: isFailureResponse = HasBogusRecords(cacheResponse.Answer); if (!isFailureResponse) isFailureResponse = HasBogusRecords(cacheResponse.Authority); break; default: isFailureResponse = true; break; } if (isFailureResponse) { //return failure response List options; if ((cacheResponse.EDNS is not null) && (cacheResponse.EDNS.Options.Count > 0)) { options = new List(cacheResponse.EDNS.Options.Count); foreach (EDnsOption option in cacheResponse.EDNS.Options) { if (option.Code == EDnsOptionCode.EXTENDED_DNS_ERROR) options.Add(option); } } else { options = null; } DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, [question], null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, cacheResponse)); } else { //return cached stale answer taskCompletionSource.SetResult(new RecursiveResolveResponse(cacheResponse, cacheResponse)); } } } else { IReadOnlyList options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Other, "Resolver exception"))]; DnsDatagram failureResponse = new DnsDatagram(0, true, DnsOpcode.StandardQuery, false, false, true, true, false, dnssecValidation, DnsResponseCode.ServerFailure, [question], null, null, null, _udpPayloadSize, dnssecValidation ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options); taskCompletionSource.SetResult(new RecursiveResolveResponse(failureResponse, failureResponse)); } } finally { _resolverTasks.TryRemove(GetResolverQueryKey(question, eDnsClientSubnet), out _); } } private async Task DefaultRecursiveResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, IDnsCache dnsCache, bool dnssecValidation, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { IReadOnlyList forwarders = _forwarders; if ((forwarders is not null) && (forwarders.Count > 0)) { //use forwarders if (_concurrentForwarding) { if (_proxy is null) { //recursive resolve forwarders only when proxy is null else let proxy resolve it to allow using .onion or private domains List newForwarders = new List(forwarders.Count); List> resolveTasks = new List>(forwarders.Count); foreach (NameServerAddress forwarder in forwarders) { if (forwarder.IsIPEndPointStale) { //refresh forwarder IPEndPoint if stale resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1) { await forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); return forwarder; }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken)); } else { newForwarders.Add(forwarder); } } Exception lastException = null; foreach (Task resolveTask in resolveTasks) { try { newForwarders.Add(await resolveTask); } catch (Exception ex) { lastException = ex; _resolverLog?.Write(ex); } } if (newForwarders.Count < 1) throw new DnsServerException("Failed to resolve forwarder domain name for all forwarders: " + forwarders.Join(), lastException); forwarders = newForwarders; } //query forwarders and update cache DnsClient dnsClient = new DnsClient(forwarders); dnsClient.Cache = dnsCache; dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; dnsClient.RandomizeName = _randomizeName; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = _forwarderConcurrency; dnsClient.UdpPayloadSize = _udpPayloadSize; dnsClient.DnssecValidation = dnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them return await dnsClient.ResolveAsync(question, cancellationToken); } else { //do sequentially ordered forwarding Exception lastException = null; foreach (NameServerAddress forwarder in forwarders) { if (_proxy is null) { //recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains if (forwarder.IsIPEndPointStale) { try { //refresh forwarder IPEndPoint if stale await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return forwarder.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken); } catch (Exception ex) { //failed to refresh forwarder IP address; try next forwarder lastException = ex; _resolverLog?.Write(ex); continue; } } } //query forwarder and update cache DnsClient dnsClient = new DnsClient(forwarder); dnsClient.Cache = dnsCache; dnsClient.Proxy = _proxy; dnsClient.PreferIPv6 = _preferIPv6; dnsClient.RandomizeName = _randomizeName; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = _forwarderConcurrency; dnsClient.UdpPayloadSize = _udpPayloadSize; dnsClient.DnssecValidation = dnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.ConditionalForwardingZoneCut = question.Name; //adding zone cut to allow CNAME domains to be resolved independently to handle cases when private/forwarder zone is configured for them try { return await dnsClient.ResolveAsync(question, cancellationToken); } catch (Exception ex) { lastException = ex; } if (dnsCache is not ResolverPrefetchDnsCache) dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache } ExceptionDispatchInfo.Capture(lastException).Throw(); throw lastException; } } else { //do recursive resolution return await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return DnsClient.RecursiveResolveAsync(question, dnsCache, _proxy, _preferIPv6, _udpPayloadSize, _randomizeName, _qnameMinimization, dnssecValidation, eDnsClientSubnet, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, true, true, null, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT, cancellationToken); } } internal async Task PriorityConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, bool skipDnsAppAuthoritativeRequestHandlers, IReadOnlyList conditionalForwarders) { if (conditionalForwarders.Count == 1) { DnsResourceRecord conditionalForwarder = conditionalForwarders[0]; return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers); } //check for forwarder name server resolution List resolveTasks = new List(conditionalForwarders.Count); foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) continue; //skip resolving NetProxy proxy = forwarder.GetProxy(_proxy); if (proxy is null) { //recursive resolve forwarder only when proxy is null else let proxy resolve it to allow using .onion or private domains if (forwarder.NameServer.IsIPEndPointStale) { //refresh forwarder IPEndPoint if stale resolveTasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return forwarder.NameServer.RecursiveResolveIPAddressAsync(dnsCache, null, _preferIPv6, _udpPayloadSize, _randomizeName, _resolverRetries, _resolverTimeout, _resolverConcurrency, _resolverMaxStackCount, cancellationToken1); }, RECURSIVE_RESOLUTION_TIMEOUT)); } } } Exception lastResolverException = null; foreach (Task resolverTask in resolveTasks) { try { await resolverTask; } catch (Exception ex) { lastResolverException = ex; _resolverLog?.Write(ex); } } //group by priority Dictionary> conditionalForwarderGroups = new Dictionary>(conditionalForwarders.Count); { foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; if (forwarder.NameServer.IsIPEndPointStale) continue; //skip stale forwarders since they failed to resolve if (conditionalForwarderGroups.TryGetValue(forwarder.Priority, out List conditionalForwardersEntry)) { conditionalForwardersEntry.Add(conditionalForwarder); } else { conditionalForwardersEntry = new List(2) { conditionalForwarder }; conditionalForwarderGroups[forwarder.Priority] = conditionalForwardersEntry; } } } if (conditionalForwarderGroups.Count < 1) { List forwarders = new List(conditionalForwarders.Count); foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; forwarders.Add((conditionalForwarder.RDATA as DnsForwarderRecordData).NameServer); } throw new DnsServerException("Failed to resolve forwarder domain name for all conditional forwarders: " + forwarders.Join(), lastResolverException); } if (conditionalForwarderGroups.Count == 1) { foreach (KeyValuePair> conditionalForwardersEntry in conditionalForwarderGroups) return await ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry.Value, skipDnsAppAuthoritativeRequestHandlers); } List priorities = new List(conditionalForwarderGroups.Keys); priorities.Sort(); using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { CancellationToken currentCancellationToken = cancellationTokenSource.Token; DnsDatagram lastResponse = null; Exception lastException = null; foreach (byte priority in priorities) { if (!conditionalForwarderGroups.TryGetValue(priority, out List conditionalForwardersEntry)) continue; Task priorityTask = ConcurrentConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwardersEntry, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken); try { DnsDatagram priorityTaskResponse = await priorityTask; //await to get response switch (priorityTaskResponse.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: cancellationTokenSource.Cancel(); //to stop other priority resolver tasks return priorityTaskResponse; default: //keep response lastResponse = priorityTaskResponse; break; } } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastException = ex; if (lastException is AggregateException) lastException = lastException.InnerException; } if (dnsCache is not ResolverPrefetchDnsCache) dnsCache = new ResolverPrefetchDnsCache(this, skipDnsAppAuthoritativeRequestHandlers, question); //to prevent low priority tasks to read failure response from cache } if (lastResponse is not null) return lastResponse; if (lastException is not null) ExceptionDispatchInfo.Capture(lastException).Throw(); throw new InvalidOperationException(); } } private async Task ConcurrentConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, List conditionalForwarders, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { if (conditionalForwarders.Count == 1) { DnsResourceRecord conditionalForwarder = conditionalForwarders[0]; return await ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, conditionalForwarder.RDATA as DnsForwarderRecordData, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, cancellationToken); } using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource()) { using CancellationTokenRegistration r = cancellationToken.Register(cancellationTokenSource.Cancel); CancellationToken currentCancellationToken = cancellationTokenSource.Token; List> tasks = new List>(conditionalForwarders.Count); //start worker tasks foreach (DnsResourceRecord conditionalForwarder in conditionalForwarders) { if (conditionalForwarder.Type != DnsResourceRecordType.FWD) continue; DnsForwarderRecordData forwarder = conditionalForwarder.RDATA as DnsForwarderRecordData; tasks.Add(Task.Factory.StartNew(delegate () { return ConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, forwarder, conditionalForwarder.Name, skipDnsAppAuthoritativeRequestHandlers, currentCancellationToken); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current).Unwrap()); } //wait for first positive response, or for all tasks to fault DnsDatagram lastResponse = null; Exception lastException = null; while (tasks.Count > 0) { Task completedTask = await Task.WhenAny(tasks); try { DnsDatagram taskResponse = await completedTask; //await to get response switch (taskResponse.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: cancellationTokenSource.Cancel(); //to stop other resolver tasks return taskResponse; default: //keep response lastResponse = taskResponse; break; } } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastException = ex; if (lastException is AggregateException) lastException = lastException.InnerException; } tasks.Remove(completedTask); } if (lastResponse is not null) return lastResponse; if (lastException is not null) ExceptionDispatchInfo.Capture(lastException).Throw(); throw new InvalidOperationException(); } } private Task ConditionalForwarderResolveAsync(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, IDnsCache dnsCache, DnsForwarderRecordData forwarder, string conditionalForwardingZoneCut, bool skipDnsAppAuthoritativeRequestHandlers, CancellationToken cancellationToken = default) { if (forwarder.Forwarder.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { //resolve via default recursive resolver with DNSSEC validation preference return DefaultRecursiveResolveAsync(question, eDnsClientSubnet, dnsCache, forwarder.DnssecValidation, skipDnsAppAuthoritativeRequestHandlers, cancellationToken); } else { //resolve via conditional forwarder DnsClient dnsClient = new DnsClient(forwarder.NameServer); dnsClient.Cache = dnsCache; dnsClient.Proxy = forwarder.GetProxy(_proxy); dnsClient.PreferIPv6 = _preferIPv6; dnsClient.RandomizeName = _randomizeName; dnsClient.Retries = _forwarderRetries; dnsClient.Timeout = _forwarderTimeout; dnsClient.Concurrency = _forwarderConcurrency; dnsClient.UdpPayloadSize = _udpPayloadSize; dnsClient.DnssecValidation = forwarder.DnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; dnsClient.AdvancedForwardingClientSubnet = advancedForwardingClientSubnet; dnsClient.ConditionalForwardingZoneCut = conditionalForwardingZoneCut; return dnsClient.ResolveAsync(question, cancellationToken); } } private DnsDatagram PrepareRecursiveResolveResponse(DnsDatagram request, RecursiveResolveResponse resolveResponse) { //get a tailored response for the request bool dnssecOk = request.DnssecOk; if (request.CheckingDisabled) { DnsDatagram cdResponse = resolveResponse.CheckingDisabledResponse; bool authenticData = false; IReadOnlyList cdAnswer; IReadOnlyList cdAuthority; IReadOnlyList cdAdditional = RemoveOPTFromAdditional(cdResponse.Additional, dnssecOk); EDnsHeaderFlags ednsFlags; if (dnssecOk) { if (cdResponse.Answer.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in cdResponse.Answer) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } else if (cdResponse.Authority.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in cdResponse.Authority) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } cdAnswer = cdResponse.Answer; cdAuthority = cdResponse.Authority; ednsFlags = EDnsHeaderFlags.DNSSEC_OK; } else { cdAnswer = FilterDnssecRecords(cdResponse.Answer); cdAuthority = FilterDnssecRecords(cdResponse.Authority); ednsFlags = EDnsHeaderFlags.None; } DnsDatagram finalCdResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, true, cdResponse.RCODE, request.Question, cdAnswer, cdAuthority, cdAdditional, _udpPayloadSize, ednsFlags, cdResponse.EDNS?.Options); DnsDatagramMetadata metadata = cdResponse.Metadata; if (metadata is not null) finalCdResponse.SetMetadata(metadata.NameServer, metadata.RoundTripTime); return finalCdResponse; } DnsResponseCode rCode; DnsDatagram response = resolveResponse.Response; IReadOnlyList answer = response.Answer; IReadOnlyList authority = response.Authority; IReadOnlyList additional = response.Additional; switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NxDomain: case DnsResponseCode.YXDomain: rCode = response.RCODE; break; default: rCode = DnsResponseCode.ServerFailure; break; } //answer section checks if (!dnssecOk && (answer.Count > 0) && (response.Question[0].Type != DnsResourceRecordType.ANY)) { //remove RRSIGs from answer bool foundRRSIG = false; foreach (DnsResourceRecord record in answer) { if (record.Type == DnsResourceRecordType.RRSIG) { foundRRSIG = true; break; } } if (foundRRSIG) { List newAnswer = new List(answer.Count); foreach (DnsResourceRecord record in answer) { if (record.Type == DnsResourceRecordType.RRSIG) continue; newAnswer.Add(record); } answer = newAnswer; } } //authority section checks if (!dnssecOk && (authority.Count > 0)) { //remove DNSSEC records bool foundDnssecRecords = false; bool foundOther = false; foreach (DnsResourceRecord record in authority) { switch (record.Type) { case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: foundDnssecRecords = true; break; default: foundOther = true; break; } } if (foundDnssecRecords) { if (foundOther) { List newAuthority = new List(2); foreach (DnsResourceRecord record in authority) { switch (record.Type) { case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: break; default: newAuthority.Add(record); break; } } authority = newAuthority; } else { authority = Array.Empty(); } } } //additional section checks if (additional.Count > 0) { if ((request.EDNS is not null) && (response.EDNS is not null) && ((response.EDNS.Options.Count > 0) || (response.DnsClientExtendedErrors.Count > 0))) { //copy options as new OPT and keep other records List newAdditional = new List(additional.Count); foreach (DnsResourceRecord record in additional) { switch (record.Type) { case DnsResourceRecordType.OPT: continue; case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.DNSKEY: if (dnssecOk) break; continue; } newAdditional.Add(record); } IReadOnlyList options; if (response.GetEDnsClientSubnetOption(true) is not null) { //response contains ECS if (request.GetEDnsClientSubnetOption(true) is not null) { //request has ECS and type is supported; keep ECS in response options = response.EDNS.Options; } else { //cache does not support the qtype so remove ECS from response if (response.EDNS.Options.Count == 1) { options = Array.Empty(); } else { List newOptions = new List(response.EDNS.Options.Count); foreach (EDnsOption option in response.EDNS.Options) { if (option.Code != EDnsOptionCode.EDNS_CLIENT_SUBNET) newOptions.Add(option); } options = newOptions; } } } else { options = response.EDNS.Options; } if (response.DnsClientExtendedErrors.Count > 0) { //add dns client extended errors List newOptions = new List(options.Count + response.DnsClientExtendedErrors.Count); newOptions.AddRange(options); foreach (EDnsExtendedDnsErrorOptionData ee in response.DnsClientExtendedErrors) newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, ee)); options = newOptions; } newAdditional.Add(DnsDatagramEdns.GetOPTFor(_udpPayloadSize, rCode, 0, request.DnssecOk ? EDnsHeaderFlags.DNSSEC_OK : EDnsHeaderFlags.None, options)); additional = newAdditional; } else if (response.EDNS is not null) { //remove OPT from additional additional = RemoveOPTFromAdditional(additional, dnssecOk); } } { bool authenticData = false; if (dnssecOk) { if (answer.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in answer) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } else if (authority.Count > 0) { authenticData = true; foreach (DnsResourceRecord record in authority) { if (record.DnssecStatus != DnssecStatus.Secure) { authenticData = false; break; } } } } DnsDatagram finalResponse = new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, true, true, authenticData, request.CheckingDisabled, rCode, request.Question, answer, authority, additional); DnsDatagramMetadata metadata = response.Metadata; if (metadata is not null) finalResponse.SetMetadata(metadata.NameServer, metadata.RoundTripTime); return finalResponse; } } private static IReadOnlyList FilterDnssecRecords(IReadOnlyList records) { foreach (DnsResourceRecord record1 in records) { switch (record1.Type) { case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: List noDnssecRecords = new List(); foreach (DnsResourceRecord record2 in records) { switch (record2.Type) { case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: break; default: noDnssecRecords.Add(record2); break; } } return noDnssecRecords; } } return records; } private static IReadOnlyList RemoveOPTFromAdditional(IReadOnlyList additional, bool dnssecOk) { if (additional.Count == 0) return additional; if ((additional.Count == 1) && (additional[0].Type == DnsResourceRecordType.OPT)) return Array.Empty(); List newAdditional = new List(additional.Count - 1); foreach (DnsResourceRecord record in additional) { switch (record.Type) { case DnsResourceRecordType.OPT: continue; case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.DNSKEY: if (dnssecOk) break; continue; } newAdditional.Add(record); } return newAdditional; } private static string GetResolverQueryKey(DnsQuestionRecord question, NetworkAddress eDnsClientSubnet) { if (eDnsClientSubnet is null) return question.ToString(); return question.ToString() + " " + eDnsClientSubnet.ToString(); } private async Task QueryCacheAsync(DnsDatagram request, bool serveStale, bool resetExpiry) { DnsDatagram cacheResponse = await _cacheZoneManager.QueryAsync(request, serveStale, false, resetExpiry); if (cacheResponse is not null) { if ((cacheResponse.RCODE != DnsResponseCode.NoError) || (cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || cacheResponse.IsFirstAuthoritySOA()) { cacheResponse.Tag = DnsServerResponseType.Cached; return cacheResponse; } } return null; } private async Task PrefetchCacheAsync(DnsQuestionRecord question, IPEndPoint remoteEP, IReadOnlyList conditionalForwarders) { try { DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [question]); _ = await RecursiveResolveAsync(request, remoteEP, conditionalForwarders, _dnssecValidation, true, false, false, _clientTimeout); } catch (Exception ex) { _resolverLog?.Write(ex); } } private async Task RefreshCacheAsync(DnsQuestionRecord neededQuestion, IList cacheRefreshSampleList, CacheRefreshSample sample, int sampleQuestionIndex) { try { //refresh cache DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [neededQuestion]); _ = await ProcessRecursiveQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Udp, sample.ConditionalForwarders, _dnssecValidation, true, false, _clientTimeout); } catch (Exception ex) { _resolverLog?.Write(ex); } finally { cacheRefreshSampleList[sampleQuestionIndex] = sample; //put back into sample list to allow refreshing it again } } private async Task GetCacheRefreshNeededQueryAsync(DnsQuestionRecord question, int trigger) { DnsDatagram cacheResponse = await QueryCacheAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { question }), false, false); if (cacheResponse is null) return question; //cache expired so refresh question if (cacheResponse.Answer.Count == 0) return null; //dont refresh empty responses //inspect response TTL values to decide if refresh is needed foreach (DnsResourceRecord answer in cacheResponse.Answer) { if ((answer.OriginalTtlValue >= _cachePrefetchEligibility) && ((answer.TTL <= trigger) || answer.IsStale)) return new DnsQuestionRecord(answer.Name, question.Type, question.Class); //TTL eligible and less than trigger so refresh for current answer record } DnsResourceRecord lastRR = cacheResponse.Answer[cacheResponse.Answer.Count - 1]; if (lastRR.Type == DnsResourceRecordType.CNAME) return new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, question.Type, question.Class); //found incomplete response; refresh the last CNAME domain name return null; //refresh not needed } private async void CachePrefetchSamplingTimerCallback(object state) { try { List> eligibleQueries = _statsManager.GetLastHourEligibleQueries(_cachePrefetchSampleEligibilityHitsPerHour); List cacheRefreshSampleList = new List(eligibleQueries.Count); int cacheRefreshTrigger = (_cachePrefetchSampleIntervalMinutes + 1) * 60; //extra 1 min to account for any delays in next sampling foreach (KeyValuePair eligibleQuery in eligibleQueries) { DnsQuestionRecord eligibleQuerySample = eligibleQuery.Key; switch (eligibleQuerySample.Type) { case DnsResourceRecordType.IXFR: case DnsResourceRecordType.AXFR: case DnsResourceRecordType.ANY: continue; //dont refresh these queries } DnsQuestionRecord refreshQuery = null; IReadOnlyList conditionalForwarders = null; //query auth zone for refresh query int queryCount = 0; bool reQueryAuthZone; do { reQueryAuthZone = false; DnsDatagram request = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { eligibleQuerySample }); DnsDatagram response = await AuthoritativeQueryAsync(request, DnsTransportProtocol.Tcp, true, false, IPENDPOINT_ANY_0); if (response is null) { //zone not hosted; do refresh refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger); } else { //zone is hosted; check further if (response.Answer.Count > 0) { DnsResourceRecord lastRR = response.GetLastAnswerRecord(); if ((lastRR.Type == DnsResourceRecordType.CNAME) && (eligibleQuerySample.Type != DnsResourceRecordType.CNAME)) { eligibleQuerySample = new DnsQuestionRecord((lastRR.RDATA as DnsCNAMERecordData).Domain, eligibleQuerySample.Type, eligibleQuerySample.Class); reQueryAuthZone = true; } } else if (response.Authority.Count > 0) { DnsResourceRecord firstAuthority = response.FindFirstAuthorityRecord(); switch (firstAuthority.Type) { case DnsResourceRecordType.NS: //zone is delegated refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger); conditionalForwarders = Array.Empty(); //do forced recursive resolution using empty conditional forwarders break; case DnsResourceRecordType.FWD: //zone is conditional forwarder refreshQuery = await GetCacheRefreshNeededQueryAsync(eligibleQuerySample, cacheRefreshTrigger); conditionalForwarders = response.Authority; //do conditional forwarding break; } } } } while (reQueryAuthZone && (++queryCount < MAX_CNAME_HOPS)); if (refreshQuery is not null) { bool alreadyExists = false; foreach (CacheRefreshSample cacheRefreshSample in cacheRefreshSampleList) { if (cacheRefreshSample.SampleQuestion.Equals(refreshQuery)) { alreadyExists = true; break; //already exists in sample list } } if (!alreadyExists) cacheRefreshSampleList.Add(new CacheRefreshSample(refreshQuery, conditionalForwarders)); } } _cacheRefreshSampleList = cacheRefreshSampleList; } catch (Exception ex) { _log.Write(ex); } finally { lock (_cachePrefetchSamplingTimerLock) { _cachePrefetchSamplingTimer?.Change(_cachePrefetchSampleIntervalMinutes * 60 * 1000, Timeout.Infinite); } } } private async void CachePrefetchRefreshTimerCallback(object state) { try { IList cacheRefreshSampleList = _cacheRefreshSampleList; if (cacheRefreshSampleList is not null) { const int MIN_TRIGGER = 10 + 4; //minimum trigger is 10 (timer interval) + 4 (additional margin for resolution delays to avoid record expiry) int cacheRefreshTrigger = _cachePrefetchTrigger < MIN_TRIGGER ? MIN_TRIGGER : _cachePrefetchTrigger; for (int i = 0; i < cacheRefreshSampleList.Count; i++) { CacheRefreshSample sample = cacheRefreshSampleList[i]; if (sample is null) continue; //currently being refreshed DnsQuestionRecord neededQuestion = await GetCacheRefreshNeededQueryAsync(sample.SampleQuestion, cacheRefreshTrigger); if (neededQuestion is null) continue; //no need to refresh for this query //run in resolver thread pool if (_resolverTaskPool.TryQueueTask(delegate (object state) { return RefreshCacheAsync(neededQuestion, cacheRefreshSampleList, sample, (int)state); }, i) ) { //refresh cache task was queued cacheRefreshSampleList[i] = null; //remove from sample list to avoid concurrent refresh attempt } } } } catch (Exception ex) { _log.Write(ex); } finally { lock (_cachePrefetchRefreshTimerLock) { _cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INTEVAL, Timeout.Infinite); } } } private void ResetPrefetchTimers() { if ((_cachePrefetchTrigger == 0) || (_recursion == DnsServerRecursion.Deny)) { lock (_cachePrefetchSamplingTimerLock) { _cachePrefetchSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite); } lock (_cachePrefetchRefreshTimerLock) { _cachePrefetchRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } else if (_state == ServiceState.Running) { lock (_cachePrefetchSamplingTimerLock) { _cachePrefetchSamplingTimer?.Change(CACHE_PREFETCH_SAMPLING_TIMER_INITIAL_INTEVAL, Timeout.Infinite); } lock (_cachePrefetchRefreshTimerLock) { _cachePrefetchRefreshTimer?.Change(CACHE_PREFETCH_REFRESH_TIMER_INTEVAL, Timeout.Infinite); } } } private bool IsQpmLimitBypassed(IPAddress remoteIP) { if (IPAddress.IsLoopback(remoteIP)) return true; if (_qpmLimitBypassList is not null) { foreach (NetworkAddress networkAddress in _qpmLimitBypassList) { if (networkAddress.Contains(remoteIP)) return true; } } return false; } private bool HasQpmLimitExceeded(NetworkAddress clientSubnet, DnsTransportProtocol protocol, (int, int) qpmLimits, IReadOnlyDictionary qpmLimitClientSubnetStats, out int qpmLimit, out int currentQpm) { qpmLimit = protocol == DnsTransportProtocol.Udp ? qpmLimits.Item1 : qpmLimits.Item2; if ((qpmLimit > 0) && qpmLimitClientSubnetStats.TryGetValue(clientSubnet, out (long, long) countPerSampleTuple)) { long countPerSample = protocol == DnsTransportProtocol.Udp ? countPerSampleTuple.Item1 : countPerSampleTuple.Item2; long averageCountPerMinute = countPerSample / _qpmLimitSampleMinutes; if (averageCountPerMinute >= qpmLimit) { currentQpm = (int)averageCountPerMinute; return true; } } currentQpm = 0; return false; } internal bool HasQpmLimitExceeded(IPAddress remoteIP, DnsTransportProtocol protocol) { if (_qpmLimitClientSubnetStats is null) return false; if ((_qpmPrefixLimitsIPv4.Count < 1) && (_qpmPrefixLimitsIPv6.Count < 1)) return false; if (IsQpmLimitBypassed(remoteIP)) return false; switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: foreach (KeyValuePair qpmPrefixLimit in _qpmPrefixLimitsIPv4) { if (HasQpmLimitExceeded(new NetworkAddress(remoteIP, (byte)qpmPrefixLimit.Key), protocol, qpmPrefixLimit.Value, _qpmLimitClientSubnetStats, out _, out _)) return true; } break; case AddressFamily.InterNetworkV6: foreach (KeyValuePair qpmPrefixLimit in _qpmPrefixLimitsIPv6) { if (HasQpmLimitExceeded(new NetworkAddress(remoteIP, (byte)qpmPrefixLimit.Key), protocol, qpmPrefixLimit.Value, _qpmLimitClientSubnetStats, out _, out _)) return true; } break; default: throw new NotSupportedException("AddressFamily not supported."); } return false; } private void QpmLimitSamplingTimerCallback(object state) { try { Dictionary qpmLimitClientSubnetStats = _statsManager.GetLatestClientSubnetStats(_qpmLimitSampleMinutes, _qpmPrefixLimitsIPv4.Keys, _qpmPrefixLimitsIPv6.Keys); WriteClientSubnetRateLimitLog(_qpmLimitClientSubnetStats, qpmLimitClientSubnetStats); _qpmLimitClientSubnetStats = qpmLimitClientSubnetStats; } catch (Exception ex) { _log.Write(ex); } finally { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(QPM_LIMIT_SAMPLING_TIMER_INTERVAL, Timeout.Infinite); } } } private void WriteClientSubnetRateLimitLog(IReadOnlyDictionary oldQpmLimitClientSubnetStats, Dictionary newQpmLimitClientSubnetStats) { if (oldQpmLimitClientSubnetStats is not null) { foreach (KeyValuePair sampleEntry in oldQpmLimitClientSubnetStats) { if (IsQpmLimitBypassed(sampleEntry.Key.GetLastAddress())) continue; //network bypassed IReadOnlyDictionary qpmPrefixLimits; switch (sampleEntry.Key.AddressFamily) { case AddressFamily.InterNetwork: qpmPrefixLimits = _qpmPrefixLimitsIPv4; break; case AddressFamily.InterNetworkV6: qpmPrefixLimits = _qpmPrefixLimitsIPv6; break; default: continue; } if (qpmPrefixLimits.TryGetValue(sampleEntry.Key.PrefixLength, out (int, int) qpmPrefixLimitValue)) { //for udp if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _)) { //previously over limit if (!HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitUdp, out int currentQpmUdp)) { //currently under limit _log.Write("Client subnet '" + sampleEntry.Key + "' is no longer being rate limited for UDP services since current query rate (" + currentQpmUdp + " qpm) is below " + qpmLimitUdp + " qpm limit."); } } //for tcp if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _)) { //previously over limit if (!HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitTcp, out int currentQpmTcp)) { //currently under limit _log.Write("Client subnet '" + sampleEntry.Key + "' is no longer being rate limited for TCP services since current query rate (" + currentQpmTcp + " qpm) is below " + qpmLimitTcp + " qpm limit."); } } } } } foreach (KeyValuePair sampleEntry in newQpmLimitClientSubnetStats) { if (IsQpmLimitBypassed(sampleEntry.Key.GetLastAddress())) continue; //network bypassed IReadOnlyDictionary qpmPrefixLimits; switch (sampleEntry.Key.AddressFamily) { case AddressFamily.InterNetwork: qpmPrefixLimits = _qpmPrefixLimitsIPv4; break; case AddressFamily.InterNetworkV6: qpmPrefixLimits = _qpmPrefixLimitsIPv6; break; default: continue; } if (qpmPrefixLimits.TryGetValue(sampleEntry.Key.PrefixLength, out (int, int) qpmPrefixLimitValue)) { //for udp if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitUdp, out int currentQpmUdp)) { //currently over limit if ((oldQpmLimitClientSubnetStats is null) || !HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Udp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _)) { //previously under limit _log.Write("Client subnet '" + sampleEntry.Key + "' is being rate limited for UDP services till the current query rate (" + currentQpmUdp + " qpm) falls below " + qpmLimitUdp + " qpm limit."); } } //for tcp if (HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, newQpmLimitClientSubnetStats, out int qpmLimitTcp, out int currentQpmTcp)) { //currently over limit if ((oldQpmLimitClientSubnetStats is null) || !HasQpmLimitExceeded(sampleEntry.Key, DnsTransportProtocol.Tcp, qpmPrefixLimitValue, oldQpmLimitClientSubnetStats, out _, out _)) { //previously under limit _log.Write("Client subnet '" + sampleEntry.Key + "' is being rate limited for TCP services till the current query rate (" + currentQpmTcp + " qpm) falls below " + qpmLimitTcp + " qpm limit."); } } } } } private bool SendQpmLimitExceededTruncationResponse() { switch (_qpmLimitUdpTruncationPercentage) { case 0: return false; case 100: return true; default: int p = RandomNumberGenerator.GetInt32(100); return p < _qpmLimitUdpTruncationPercentage; } } private void ResetQpsLimitTimer() { if ((_qpmPrefixLimitsIPv4.Count < 1) && (_qpmPrefixLimitsIPv6.Count < 1)) { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(Timeout.Infinite, Timeout.Infinite); _qpmLimitClientSubnetStats = null; } } else if (_state == ServiceState.Running) { lock (_qpmLimitSamplingTimerLock) { _qpmLimitSamplingTimer?.Change(0, Timeout.Infinite); } } } private void UpdateThisServer() { foreach (IPEndPoint localEndPoint in _localEndPoints) { if (localEndPoint.Address.Equals(IPAddress.Any)) { _thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.Loopback, localEndPoint.Port)); return; } if (localEndPoint.Address.Equals(IPAddress.IPv6Any)) { _thisServer = new NameServerAddress(_serverDomain, new IPEndPoint(IPAddress.IPv6Loopback, localEndPoint.Port)); return; } } _thisServer = new NameServerAddress(_serverDomain, _localEndPoints[0]); } #endregion #region resolver task pool internal bool TryQueueResolverTask(Func task, object state = null) { return _resolverTaskPool.TryQueueTask(task, state); } private void ReconfigureResolverTaskPool(ushort maxConcurrentResolutionsPerCore) { TaskPool previousResolverTaskPool = _resolverTaskPool; int maxConcurrentResolutions = Environment.ProcessorCount * maxConcurrentResolutionsPerCore; int resolverQueueSize = maxConcurrentResolutions * 5 * 10; //assuming 5 qps average resolution rate for 10 sec _resolverTaskPool = new TaskPool(resolverQueueSize, maxConcurrentResolutions, _resolverTaskScheduler); previousResolverTaskPool?.Dispose(); //stop previous task pool from queuing new tasks and complete reading } #endregion #region doh web service private async Task StartDoHAsync(bool throwIfBindFails) { IReadOnlyList localAddresses = WebUtilities.GetValidKestrelLocalAddresses(_localEndPoints.Convert(delegate (IPEndPoint ep) { return ep.Address; })); try { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.Environment.ContentRootFileProvider = new PhysicalFileProvider(Path.GetDirectoryName(_dohwwwFolder)) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.Environment.WebRootFileProvider = new PhysicalFileProvider(_dohwwwFolder) { UseActivePolling = true, UsePollingFileWatcher = true }; builder.WebHost.ConfigureKestrel(delegate (WebHostBuilderContext context, KestrelServerOptions serverOptions) { //bind to http port if (_enableDnsOverHttp) { foreach (IPAddress localAddress in localAddresses) serverOptions.Listen(localAddress, _dnsOverHttpPort); } //bind to https port if (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null)) { foreach (IPAddress localAddress in localAddresses) { serverOptions.Listen(localAddress, _dnsOverHttpsPort, delegate (ListenOptions listenOptions) { if (_enableDnsOverHttp3) 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(_dohSslServerAuthenticationOptions); }, null); }); } } serverOptions.AddServerHeader = false; serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout); serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMilliseconds(_tcpReceiveTimeout); serverOptions.Limits.MaxRequestHeadersTotalSize = 4096; serverOptions.Limits.MaxRequestLineSize = serverOptions.Limits.MaxRequestHeadersTotalSize; serverOptions.Limits.MaxRequestBufferSize = serverOptions.Limits.MaxRequestLineSize; serverOptions.Limits.MaxRequestBodySize = 64 * 1024; serverOptions.Limits.MaxResponseBufferSize = 4096; }); builder.Logging.ClearProviders(); _dohWebService = builder.Build(); _dohWebService.UseDefaultFiles(); _dohWebService.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 }); _dohWebService.UseRouting(); _dohWebService.MapGet("/dns-query", ProcessDoHRequestAsync); _dohWebService.MapPost("/dns-query", ProcessDoHRequestAsync); await _dohWebService.StartAsync(); foreach (IPAddress localAddress in localAddresses) { if (_enableDnsOverHttp) _log.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server was bound successfully."); if (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null)) _log.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server was bound successfully."); } } catch (Exception ex) { await StopDoHAsync(); foreach (IPAddress localAddress in localAddresses) { if (_enableDnsOverHttp) _log.Write(new IPEndPoint(localAddress, _dnsOverHttpPort), "Http", "DNS Server failed to bind."); if (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null)) _log.Write(new IPEndPoint(localAddress, _dnsOverHttpsPort), "Https", "DNS Server failed to bind."); } _log.Write(ex); if (throwIfBindFails) throw; } } private async Task StopDoHAsync() { if (_dohWebService is not null) { try { await _dohWebService.DisposeAsync(); } catch (Exception ex) { _log.Write(ex); } _dohWebService = null; } } private bool IsHttp2Supported() { if (_enableDnsOverHttp3) 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; } } #endregion #region public public async Task StartAsync(bool throwIfBindFails = false) { if (_disposed) ObjectDisposedException.ThrowIf(_disposed, this); if (_state != ServiceState.Stopped) throw new InvalidOperationException("DNS Server is already running."); _state = ServiceState.Starting; //bind on all local end points foreach (IPEndPoint localEP in _localEndPoints) { Socket udpListener = null; try { udpListener = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp); #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom() if (Environment.OSVersion.Platform == PlatformID.Win32NT) { const uint IOC_IN = 0x80000000; const uint IOC_VENDOR = 0x18000000; const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; udpListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); } #endregion if (Environment.OSVersion.Platform == PlatformID.Unix) udpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses udpListener.ReceiveBufferSize = 512 * 1024; udpListener.SendBufferSize = 512 * 1024; try { udpListener.Bind(localEP); } catch (SocketException ex1) { switch (ex1.ErrorCode) { case 99: //SocketException (99): Cannot assign requested address await Task.Delay(5000); //wait for address to be available before retrying udpListener.Bind(localEP); break; default: throw; } } _udpListeners.Add(udpListener); _log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server was bound successfully."); } catch (Exception ex) { _log.Write(localEP, DnsTransportProtocol.Udp, "DNS Server failed to bind.\r\n" + ex.ToString()); udpListener?.Dispose(); if (throwIfBindFails) throw; } if (_enableDnsOverUdpProxy) { IPEndPoint udpProxyEP = new IPEndPoint(localEP.Address, _dnsOverUdpProxyPort); Socket udpProxyListener = null; try { udpProxyListener = new Socket(udpProxyEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp); #region this code ignores ICMP port unreachable responses which creates SocketException in ReceiveFrom() if (Environment.OSVersion.Platform == PlatformID.Win32NT) { const uint IOC_IN = 0x80000000; const uint IOC_VENDOR = 0x18000000; const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; udpProxyListener.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null); } #endregion if (Environment.OSVersion.Platform == PlatformID.Unix) udpProxyListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses udpProxyListener.ReceiveBufferSize = 512 * 1024; udpProxyListener.SendBufferSize = 512 * 1024; udpProxyListener.Bind(udpProxyEP); _udpProxyListeners.Add(udpProxyListener); _log.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, "DNS Server was bound successfully."); } catch (Exception ex) { _log.Write(udpProxyEP, DnsTransportProtocol.UdpProxy, "DNS Server failed to bind.\r\n" + ex.ToString()); udpProxyListener?.Dispose(); if (throwIfBindFails) throw; } } Socket tcpListener = null; try { tcpListener = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); if (Environment.OSVersion.Platform == PlatformID.Unix) tcpListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses tcpListener.Bind(localEP); tcpListener.Listen(_listenBacklog); _tcpListeners.Add(tcpListener); _log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server was bound successfully."); } catch (Exception ex) { _log.Write(localEP, DnsTransportProtocol.Tcp, "DNS Server failed to bind.\r\n" + ex.ToString()); tcpListener?.Dispose(); if (throwIfBindFails) throw; } if (_enableDnsOverTcpProxy) { IPEndPoint tcpProxyEP = new IPEndPoint(localEP.Address, _dnsOverTcpProxyPort); Socket tcpProxyListner = null; try { tcpProxyListner = new Socket(tcpProxyEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); if (Environment.OSVersion.Platform == PlatformID.Unix) tcpProxyListner.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses tcpProxyListner.Bind(tcpProxyEP); tcpProxyListner.Listen(_listenBacklog); _tcpProxyListeners.Add(tcpProxyListner); _log.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, "DNS Server was bound successfully."); } catch (Exception ex) { _log.Write(tcpProxyEP, DnsTransportProtocol.TcpProxy, "DNS Server failed to bind.\r\n" + ex.ToString()); tcpProxyListner?.Dispose(); if (throwIfBindFails) throw; } } if (_enableDnsOverTls && (_dotSslServerAuthenticationOptions is not null)) { IPEndPoint tlsEP = new IPEndPoint(localEP.Address, _dnsOverTlsPort); Socket tlsListener = null; try { tlsListener = new Socket(tlsEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp); if (Environment.OSVersion.Platform == PlatformID.Unix) tlsListener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); //to allow binding to same port with different addresses tlsListener.Bind(tlsEP); tlsListener.Listen(_listenBacklog); _tlsListeners.Add(tlsListener); _log.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server was bound successfully."); } catch (Exception ex) { _log.Write(tlsEP, DnsTransportProtocol.Tls, "DNS Server failed to bind.\r\n" + ex.ToString()); tlsListener?.Dispose(); if (throwIfBindFails) throw; } } if (_enableDnsOverQuic && (_doqSslServerAuthenticationOptions is not null)) { IPEndPoint quicEP = new IPEndPoint(localEP.Address, _dnsOverQuicPort); QuicListener quicListener = null; try { QuicListenerOptions listenerOptions = new QuicListenerOptions() { ListenEndPoint = quicEP, ListenBacklog = _listenBacklog, ApplicationProtocols = _doqApplicationProtocols, ConnectionOptionsCallback = delegate (QuicConnection quicConnection, SslClientHelloInfo sslClientHello, CancellationToken cancellationToken) { QuicServerConnectionOptions serverConnectionOptions = new QuicServerConnectionOptions() { DefaultCloseErrorCode = (long)DnsOverQuicErrorCodes.DOQ_NO_ERROR, DefaultStreamErrorCode = (long)DnsOverQuicErrorCodes.DOQ_UNSPECIFIED_ERROR, MaxInboundUnidirectionalStreams = 0, MaxInboundBidirectionalStreams = _quicMaxInboundStreams, IdleTimeout = TimeSpan.FromMilliseconds(_quicIdleTimeout), ServerAuthenticationOptions = _doqSslServerAuthenticationOptions }; return ValueTask.FromResult(serverConnectionOptions); } }; quicListener = await QuicListener.ListenAsync(listenerOptions); _quicListeners.Add(quicListener); _log.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server was bound successfully."); } catch (Exception ex) { _log.Write(quicEP, DnsTransportProtocol.Quic, "DNS Server failed to bind.\r\n" + ex.ToString()); if (quicListener is not null) await quicListener.DisposeAsync(); if (throwIfBindFails) throw; } } } //start reading query packets int listenerTaskCount = Environment.ProcessorCount; foreach (Socket udpListener in _udpListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return ReadUdpRequestAsync(udpListener, DnsTransportProtocol.Udp); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket udpProxyListener in _udpProxyListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return ReadUdpRequestAsync(udpProxyListener, DnsTransportProtocol.UdpProxy); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket tcpListener in _tcpListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptConnectionAsync(tcpListener, DnsTransportProtocol.Tcp); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket tcpProxyListener in _tcpProxyListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptConnectionAsync(tcpProxyListener, DnsTransportProtocol.TcpProxy); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (Socket tlsListener in _tlsListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptConnectionAsync(tlsListener, DnsTransportProtocol.Tls); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } foreach (QuicListener quicListener in _quicListeners) { for (int i = 0; i < listenerTaskCount; i++) { _ = Task.Factory.StartNew(delegate () { return AcceptQuicConnectionAsync(quicListener); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, _queryTaskScheduler); } } if (_enableDnsOverHttp || (_enableDnsOverHttps && (_dohSslServerAuthenticationOptions is not null))) await StartDoHAsync(throwIfBindFails); _cachePrefetchSamplingTimer = new Timer(CachePrefetchSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _cachePrefetchRefreshTimer = new Timer(CachePrefetchRefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _qpmLimitSamplingTimer = new Timer(QpmLimitSamplingTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _state = ServiceState.Running; UpdateThisServer(); ResetPrefetchTimers(); ResetQpsLimitTimer(); } public async Task StopAsync() { if (_state != ServiceState.Running) return; _state = ServiceState.Stopping; lock (_cachePrefetchSamplingTimerLock) { if (_cachePrefetchSamplingTimer is not null) { _cachePrefetchSamplingTimer.Dispose(); _cachePrefetchSamplingTimer = null; } } lock (_cachePrefetchRefreshTimerLock) { if (_cachePrefetchRefreshTimer is not null) { _cachePrefetchRefreshTimer.Dispose(); _cachePrefetchRefreshTimer = null; } } lock (_qpmLimitSamplingTimerLock) { if (_qpmLimitSamplingTimer is not null) { _qpmLimitSamplingTimer.Dispose(); _qpmLimitSamplingTimer = null; } } foreach (Socket udpListener in _udpListeners) { try { udpListener.Dispose(); } catch (Exception ex) { _log.Write(ex); } } foreach (Socket udpProxyListener in _udpProxyListeners) { try { udpProxyListener.Dispose(); } catch (Exception ex) { _log.Write(ex); } } foreach (Socket tcpListener in _tcpListeners) { try { tcpListener.Dispose(); } catch (Exception ex) { _log.Write(ex); } } foreach (Socket tcpProxyListener in _tcpProxyListeners) { try { tcpProxyListener.Dispose(); } catch (Exception ex) { _log.Write(ex); } } foreach (Socket tlsListener in _tlsListeners) { try { tlsListener.Dispose(); } catch (Exception ex) { _log.Write(ex); } } foreach (QuicListener quicListener in _quicListeners) { try { await quicListener.DisposeAsync(); } catch (Exception ex) { _log.Write(ex); } } _udpListeners.Clear(); _udpProxyListeners.Clear(); _tcpListeners.Clear(); _tcpProxyListeners.Clear(); _tlsListeners.Clear(); _quicListeners.Clear(); await StopDoHAsync(); _state = ServiceState.Stopped; } public Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false, CancellationToken cancellationToken = default) { return DirectQueryAsync(new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, [question]), timeout, skipDnsAppAuthoritativeRequestHandlers, cancellationToken); } public Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, bool skipDnsAppAuthoritativeRequestHandlers = false, CancellationToken cancellationToken = default) { return TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return ProcessQueryAsync(request, IPENDPOINT_ANY_0, DnsTransportProtocol.Tcp, true, skipDnsAppAuthoritativeRequestHandlers, timeout, null); }, timeout, cancellationToken); } Task IDnsClient.ResolveAsync(DnsQuestionRecord question, CancellationToken cancellationToken) { return DirectQueryAsync(question, cancellationToken: cancellationToken); } #endregion #region properties public string ServerDomain { get { return _serverDomain; } set { if (!_serverDomain.Equals(value)) { if (DnsClient.IsDomainNameUnicode(value)) value = DnsClient.ConvertDomainNameToAscii(value); DnsClient.IsDomainNameValid(value, true); if (IPAddress.TryParse(value, out _)) throw new DnsServerException("Invalid domain name [" + value + "]: IP address cannot be used for DNS server domain name."); _serverDomain = value.ToLowerInvariant(); _fallbackResponsiblePerson = new MailAddress("hostadmin@" + _serverDomain); _authZoneManager.TriggerUpdateServerDomain(); _allowedZoneManager.UpdateServerDomain(); _blockedZoneManager.UpdateServerDomain(); _blockListZoneManager.UpdateServerDomain(); UpdateThisServer(); } } } public string ConfigFolder { get { return _configFolder; } } public IReadOnlyList LocalEndPoints { get { return _localEndPoints; } set { if ((value is null) || (value.Count == 0)) { _localEndPoints = [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)]; } else { foreach (IPEndPoint ep in value) { if (ep.Port == 853) throw new ArgumentException("Port 853 is reserved for DNS-over-TLS service. Please use a different port for DNS Server Local End Points.", nameof(LocalEndPoints)); } _localEndPoints = value; } } } public LogManager LogManager { get { return _log; } } internal MailAddress DefaultResponsiblePerson { get { return _defaultResponsiblePerson; } set { _defaultResponsiblePerson = value; } } public MailAddress ResponsiblePerson { get { if (_defaultResponsiblePerson is not null) return _defaultResponsiblePerson; if (_fallbackResponsiblePerson is null) _fallbackResponsiblePerson = new MailAddress("hostadmin@" + _serverDomain); return _fallbackResponsiblePerson; } } public NameServerAddress ThisServer { get { return _thisServer; } } public AuthZoneManager AuthZoneManager { get { return _authZoneManager; } } public AllowedZoneManager AllowedZoneManager { get { return _allowedZoneManager; } } public BlockedZoneManager BlockedZoneManager { get { return _blockedZoneManager; } } public BlockListZoneManager BlockListZoneManager { get { return _blockListZoneManager; } } public CacheZoneManager CacheZoneManager { get { return _cacheZoneManager; } } public DnsApplicationManager DnsApplicationManager { get { return _dnsApplicationManager; } } public IDnsCache DnsCache { get { return _dnsCache; } } public StatsManager StatsManager { get { return _statsManager; } } public IReadOnlyCollection ZoneTransferAllowedNetworks { get { return _zoneTransferAllowedNetworks; } set { if ((value is null) || (value.Count == 0)) _zoneTransferAllowedNetworks = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ZoneTransferAllowedNetworks), "Networks cannot have more than 255 entries."); else _zoneTransferAllowedNetworks = value; } } public IReadOnlyCollection NotifyAllowedNetworks { get { return _notifyAllowedNetworks; } set { if ((value is null) || (value.Count == 0)) _notifyAllowedNetworks = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(NotifyAllowedNetworks), "Networks cannot have more than 255 entries."); else _notifyAllowedNetworks = value; } } public bool PreferIPv6 { get { return _preferIPv6; } set { if (_preferIPv6 != value) { _preferIPv6 = value; //init udp socket pool async for port randomization ThreadPool.QueueUserWorkItem(delegate (object state) { try { if (_enableUdpSocketPool) UdpClientConnection.CreateSocketPool(_preferIPv6); } catch (Exception ex) { _log.Write(ex); } }); } } } public bool EnableUdpSocketPool { get { return _enableUdpSocketPool; } set { if (_enableUdpSocketPool != value) { _enableUdpSocketPool = value; //init udp socket pool async for port randomization ThreadPool.QueueUserWorkItem(delegate (object state) { try { if (_enableUdpSocketPool) UdpClientConnection.CreateSocketPool(_preferIPv6); else UdpClientConnection.DisposeSocketPool(); } catch (Exception ex) { _log.Write(ex); } }); } } } public ushort UdpPayloadSize { get { return _udpPayloadSize; } set { if ((value < 512) || (value > 4096)) throw new ArgumentOutOfRangeException(nameof(UdpPayloadSize), "Invalid EDNS UDP payload size: valid range is 512-4096 bytes."); _udpPayloadSize = value; } } public bool DnssecValidation { get { return _dnssecValidation; } set { if (_dnssecValidation != value) { if (!_dnssecValidation) _cacheZoneManager.Flush(); //flush cache to remove non validated data _dnssecValidation = value; } } } public bool EDnsClientSubnet { get { return _eDnsClientSubnet; } set { if (_eDnsClientSubnet != value) { _eDnsClientSubnet = value; if (!_eDnsClientSubnet) { ThreadPool.QueueUserWorkItem(delegate (object state) { try { _cacheZoneManager.DeleteEDnsClientSubnetData(); } catch (Exception ex) { _log.Write(ex); } }); } } } } public byte EDnsClientSubnetIPv4PrefixLength { get { return _eDnsClientSubnetIPv4PrefixLength; } set { if (value > 32) throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv4PrefixLength), "EDNS Client Subnet IPv4 prefix length cannot be greater than 32."); _eDnsClientSubnetIPv4PrefixLength = value; } } public byte EDnsClientSubnetIPv6PrefixLength { get { return _eDnsClientSubnetIPv6PrefixLength; } set { if (value > 64) throw new ArgumentOutOfRangeException(nameof(EDnsClientSubnetIPv6PrefixLength), "EDNS Client Subnet IPv6 prefix length cannot be greater than 64."); _eDnsClientSubnetIPv6PrefixLength = value; } } public NetworkAddress EDnsClientSubnetIpv4Override { get { return _eDnsClientSubnetIpv4Override; } set { if (value is not null) { if (value.AddressFamily != AddressFamily.InterNetwork) throw new ArgumentException("EDNS Client Subnet IPv4 Override must be an IPv4 network address.", nameof(EDnsClientSubnetIpv4Override)); if (value.IsHostAddress) value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv4PrefixLength); } _eDnsClientSubnetIpv4Override = value; } } public NetworkAddress EDnsClientSubnetIpv6Override { get { return _eDnsClientSubnetIpv6Override; } set { if (value is not null) { if (value.AddressFamily != AddressFamily.InterNetworkV6) throw new ArgumentException("EDNS Client Subnet IPv6 Override must be an IPv6 network address.", nameof(EDnsClientSubnetIpv6Override)); if (value.IsHostAddress) value = new NetworkAddress(value.Address, _eDnsClientSubnetIPv6PrefixLength); } _eDnsClientSubnetIpv6Override = value; } } public IReadOnlyDictionary QpmPrefixLimitsIPv4 { get { return _qpmPrefixLimitsIPv4; } set { if (value is null) { _qpmPrefixLimitsIPv4 = new Dictionary(); } else if (value.Count > byte.MaxValue) { throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv4), "QPM Prefix Limits for IPv4 cannot have more than 255 entries."); } else { foreach (KeyValuePair qpmPrefixLimit in value) { if ((qpmPrefixLimit.Key < 0) || (qpmPrefixLimit.Key > 32)) throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv4), "QPM limit IPv4 prefix valid range is between 0 and 32."); if ((qpmPrefixLimit.Value.Item1 < 0) || (qpmPrefixLimit.Value.Item2 < 0)) throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv4), "QPM limit value cannot be less than 0."); } _qpmPrefixLimitsIPv4 = value; } } } public IReadOnlyDictionary QpmPrefixLimitsIPv6 { get { return _qpmPrefixLimitsIPv6; } set { if (value is null) { _qpmPrefixLimitsIPv6 = new Dictionary(); } else if (value.Count > byte.MaxValue) { throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv6), "QPM Prefix Limits for IPv6 cannot have more than 255 entries."); } else { foreach (KeyValuePair qpmPrefixLimit in value) { if ((qpmPrefixLimit.Key < 0) || (qpmPrefixLimit.Key > 128)) throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv6), "QPM limit IPv6 prefix valid range is between 0 and 128."); if ((qpmPrefixLimit.Value.Item1 < 0) || (qpmPrefixLimit.Value.Item2 < 0)) throw new ArgumentOutOfRangeException(nameof(QpmPrefixLimitsIPv6), "QPM limit value cannot be less than 0."); } _qpmPrefixLimitsIPv6 = value; } } } public int QpmLimitSampleMinutes { get { return _qpmLimitSampleMinutes; } set { if ((value < 1) || (value > 60)) throw new ArgumentOutOfRangeException(nameof(QpmLimitSampleMinutes), "Valid range is between 1 and 60 minutes."); _qpmLimitSampleMinutes = value; } } public int QpmLimitUdpTruncationPercentage { get { return _qpmLimitUdpTruncationPercentage; } set { if ((value < 0) || (value > 100)) throw new ArgumentOutOfRangeException(nameof(QpmLimitUdpTruncationPercentage), "Percentage value valid range is between 0 and 100."); _qpmLimitUdpTruncationPercentage = value; } } public IReadOnlyCollection QpmLimitBypassList { get { return _qpmLimitBypassList; } set { if ((value is null) || (value.Count == 0)) _qpmLimitBypassList = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(QpmLimitBypassList), "Networks cannot have more than 255 entries."); else _qpmLimitBypassList = value; } } public int ClientTimeout { get { return _clientTimeout; } set { if ((value < 1000) || (value > 10000)) throw new ArgumentOutOfRangeException(nameof(ClientTimeout), "Valid range is from 1000 to 10000."); _clientTimeout = value; } } public int TcpSendTimeout { get { return _tcpSendTimeout; } set { if ((value < 1000) || (value > 90000)) throw new ArgumentOutOfRangeException(nameof(TcpSendTimeout), "Valid range is from 1000 to 90000."); _tcpSendTimeout = value; } } public int TcpReceiveTimeout { get { return _tcpReceiveTimeout; } set { if ((value < 1000) || (value > 90000)) throw new ArgumentOutOfRangeException(nameof(TcpReceiveTimeout), "Valid range is from 1000 to 90000."); _tcpReceiveTimeout = value; } } public int QuicIdleTimeout { get { return _quicIdleTimeout; } set { if ((value < 1000) || (value > 90000)) throw new ArgumentOutOfRangeException(nameof(QuicIdleTimeout), "Valid range is from 1000 to 90000."); _quicIdleTimeout = value; } } public int QuicMaxInboundStreams { get { return _quicMaxInboundStreams; } set { if ((value < 0) || (value > 1000)) throw new ArgumentOutOfRangeException(nameof(QuicMaxInboundStreams), "Valid range is from 1 to 1000."); _quicMaxInboundStreams = value; } } public int ListenBacklog { get { return _listenBacklog; } set { _listenBacklog = value; } } public ushort MaxConcurrentResolutionsPerCore { get { return Convert.ToUInt16(_resolverTaskPool.MaximumConcurrencyLevel / Environment.ProcessorCount); } set { if (value < 1) throw new ArgumentOutOfRangeException(nameof(MaxConcurrentResolutionsPerCore), "Value cannot be less than 1."); if (MaxConcurrentResolutionsPerCore != value) ReconfigureResolverTaskPool(value); } } public bool EnableDnsOverUdpProxy { get { return _enableDnsOverUdpProxy; } set { _enableDnsOverUdpProxy = value; } } public bool EnableDnsOverTcpProxy { get { return _enableDnsOverTcpProxy; } set { _enableDnsOverTcpProxy = value; } } public bool EnableDnsOverHttp { get { return _enableDnsOverHttp; } set { _enableDnsOverHttp = value; } } public bool EnableDnsOverTls { get { return _enableDnsOverTls; } set { _enableDnsOverTls = value; } } public bool EnableDnsOverHttps { get { return _enableDnsOverHttps; } set { _enableDnsOverHttps = value; } } public bool EnableDnsOverHttp3 { get { return _enableDnsOverHttp3; } set { _enableDnsOverHttp3 = value; } } public bool EnableDnsOverQuic { get { return _enableDnsOverQuic; } set { _enableDnsOverQuic = value; } } public IReadOnlyCollection ReverseProxyNetworkACL { get { return _reverseProxyNetworkACL; } set { if ((value is null) || (value.Count == 0)) _reverseProxyNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ReverseProxyNetworkACL), "Network Access Control List cannot have more than 255 entries."); else _reverseProxyNetworkACL = value; } } public int DnsOverUdpProxyPort { get { return _dnsOverUdpProxyPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverUdpProxyPort), "Port number valid range is from 0 to 65535."); _dnsOverUdpProxyPort = value; } } public int DnsOverTcpProxyPort { get { return _dnsOverTcpProxyPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverTcpProxyPort), "Port number valid range is from 0 to 65535."); _dnsOverTcpProxyPort = value; } } public int DnsOverHttpPort { get { return _dnsOverHttpPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), "Port number valid range is from 0 to 65535."); if (value == 53) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), "Port 53 cannot be used for DNS-over-HTTP service. Please use a different port."); if (value == 853) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpPort), "Port 853 is reserved for DNS-over-TLS service. Please use a different port for DNS-over-HTTP service."); _dnsOverHttpPort = value; } } public int DnsOverTlsPort { get { return _dnsOverTlsPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverTlsPort), "Port number valid range is from 0 to 65535."); if (value == 53) throw new ArgumentOutOfRangeException(nameof(DnsOverTlsPort), "Port 53 cannot be used for DNS-over-TLS service. Please use a different port."); _dnsOverTlsPort = value; } } public int DnsOverHttpsPort { get { return _dnsOverHttpsPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), "Port number valid range is from 0 to 65535."); if (value == 53) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), "Port 53 cannot be used for DNS-over-HTTPS service. Please use a different port."); if (value == 853) throw new ArgumentOutOfRangeException(nameof(DnsOverHttpsPort), "Port 853 is reserved for DNS-over-TLS service. Please use a different port for DNS-over-HTTPS service."); _dnsOverHttpsPort = value; } } public int DnsOverQuicPort { get { return _dnsOverQuicPort; } set { if ((value < ushort.MinValue) || (value > ushort.MaxValue)) throw new ArgumentOutOfRangeException(nameof(DnsOverQuicPort), "Port number valid range is from 0 to 65535."); if (value == 53) throw new ArgumentOutOfRangeException(nameof(DnsOverQuicPort), "Port 53 cannot be used for DNS-over-QUIC service. Please use a different port."); _dnsOverQuicPort = value; } } public string DnsTlsCertificatePath { get { return _dnsTlsCertificatePath; } } public string DnsTlsCertificatePassword { get { return _dnsTlsCertificatePassword; } } public string DnsOverHttpRealIpHeader { get { return _dnsOverHttpRealIpHeader; } set { if (string.IsNullOrEmpty(value)) _dnsOverHttpRealIpHeader = "X-Real-IP"; else if (value.Length > 255) throw new ArgumentException("DNS-over-HTTP Real IP header name cannot exceed 255 characters.", nameof(DnsOverHttpRealIpHeader)); else if (value.Contains(' ')) throw new ArgumentException("DNS-over-HTTP Real IP header name cannot contain invalid characters.", nameof(DnsOverHttpRealIpHeader)); else _dnsOverHttpRealIpHeader = value; } } public IReadOnlyDictionary TsigKeys { get { return _tsigKeys; } set { if ((value is null) || (value.Count == 0)) _tsigKeys = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(TsigKeys), "TSIG keys cannot have more than 255 entries."); else _tsigKeys = value; } } public DnsServerRecursion Recursion { get { return _recursion; } set { if (_recursion != value) { if ((_recursion == DnsServerRecursion.Deny) || (value == DnsServerRecursion.Deny)) { _recursion = value; ResetPrefetchTimers(); } else { _recursion = value; } } } } public IReadOnlyCollection RecursionNetworkACL { get { return _recursionNetworkACL; } set { if ((value is null) || (value.Count == 0)) _recursionNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(RecursionNetworkACL), "Network Access Control List cannot have more than 255 entries."); else _recursionNetworkACL = value; } } public bool RandomizeName { get { return _randomizeName; } set { _randomizeName = value; } } public bool QnameMinimization { get { return _qnameMinimization; } set { _qnameMinimization = value; } } public int ResolverRetries { get { return _resolverRetries; } set { if ((value < 1) || (value > 10)) throw new ArgumentOutOfRangeException(nameof(ResolverRetries), "Valid range is from 1 to 10."); _resolverRetries = value; } } public int ResolverTimeout { get { return _resolverTimeout; } set { if ((value < 1000) || (value > 10000)) throw new ArgumentOutOfRangeException(nameof(ResolverTimeout), "Valid range is from 1000 to 10000."); _resolverTimeout = value; } } public int ResolverConcurrency { get { return _resolverConcurrency; } set { if ((value < 1) || (value > 4)) throw new ArgumentOutOfRangeException(nameof(ResolverConcurrency), "Valid range is from 1 to 4."); _resolverConcurrency = value; } } public int ResolverMaxStackCount { get { return _resolverMaxStackCount; } set { if ((value < 10) || (value > 30)) throw new ArgumentOutOfRangeException(nameof(ResolverMaxStackCount), "Valid range is from 10 to 30."); _resolverMaxStackCount = value; } } public bool SaveCacheToDisk { get { return _saveCacheToDisk; } set { _saveCacheToDisk = value; if (!_saveCacheToDisk) { try { _cacheZoneManager.DeleteCacheZoneFile(); } catch (Exception ex) { _log.Write(ex); } } } } public bool ServeStale { get { return _serveStale; } set { _serveStale = value; } } public int ServeStaleMaxWaitTime { get { return _serveStaleMaxWaitTime; } set { if ((value < 0) || (value > 1800)) throw new ArgumentOutOfRangeException(nameof(ServeStaleMaxWaitTime), "Serve stale max wait time valid range is 0 to 1800 milliseconds. Default value is 1800 milliseconds."); _serveStaleMaxWaitTime = value; } } public int CachePrefetchEligibility { get { return _cachePrefetchEligibility; } set { if (value < 2) throw new ArgumentOutOfRangeException(nameof(CachePrefetchEligibility), "Valid value is greater that or equal to 2."); _cachePrefetchEligibility = value; } } public int CachePrefetchTrigger { get { return _cachePrefetchTrigger; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(CachePrefetchTrigger), "Valid value is greater that or equal to 0."); if (_cachePrefetchTrigger != value) { if ((_cachePrefetchTrigger == 0) || (value == 0)) { _cachePrefetchTrigger = value; ResetPrefetchTimers(); } else { _cachePrefetchTrigger = value; } } } } public int CachePrefetchSampleIntervalMinutes { get { return _cachePrefetchSampleIntervalMinutes; } set { if ((value < 1) || (value > 60)) throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleIntervalMinutes), "Valid range is between 1 and 60 minutes."); if (_cachePrefetchSampleIntervalMinutes != value) { _cachePrefetchSampleIntervalMinutes = value; ResetPrefetchTimers(); } } } public int CachePrefetchSampleEligibilityHitsPerHour { get { return _cachePrefetchSampleEligibilityHitsPerHour; } set { if (value < 1) throw new ArgumentOutOfRangeException(nameof(CachePrefetchSampleEligibilityHitsPerHour), "Valid value is greater than or equal to 1."); _cachePrefetchSampleEligibilityHitsPerHour = value; } } public bool EnableBlocking { get { return _enableBlocking; } set { _enableBlocking = value; if (_enableBlocking) _blockListZoneManager.StopTemporaryDisableBlockingTimer(); } } public bool AllowTxtBlockingReport { get { return _allowTxtBlockingReport; } set { _allowTxtBlockingReport = value; } } public IReadOnlyCollection BlockingBypassList { get { return _blockingBypassList; } set { if ((value is null) || (value.Count == 0)) _blockingBypassList = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(BlockingBypassList), "Networks cannot have more than 255 entries."); else _blockingBypassList = value; } } public DnsServerBlockingType BlockingType { get { return _blockingType; } set { _blockingType = value; } } public uint BlockingAnswerTtl { get { return _blockingAnswerTtl; } set { if (_blockingAnswerTtl != value) { _blockingAnswerTtl = value; //update SOA MINIMUM values _blockedZoneManager.UpdateServerDomain(); _blockListZoneManager.UpdateServerDomain(); } } } public IReadOnlyCollection CustomBlockingARecords { get { return _customBlockingARecords; } set { if (value is null) value = []; _customBlockingARecords = value; } } public IReadOnlyCollection CustomBlockingAAAARecords { get { return _customBlockingAAAARecords; } set { if (value is null) value = []; _customBlockingAAAARecords = value; } } public NetProxy Proxy { get { return _proxy; } set { _proxy = value; } } public IReadOnlyList Forwarders { get { return _forwarders; } set { _forwarders = value; } } public bool ConcurrentForwarding { get { return _concurrentForwarding; } set { _concurrentForwarding = value; } } public int ForwarderRetries { get { return _forwarderRetries; } set { if ((value < 1) || (value > 10)) throw new ArgumentOutOfRangeException(nameof(ForwarderRetries), "Valid range is from 1 to 10."); _forwarderRetries = value; } } public int ForwarderTimeout { get { return _forwarderTimeout; } set { if ((value < 1000) || (value > 10000)) throw new ArgumentOutOfRangeException(nameof(ForwarderTimeout), "Valid range is from 1000 to 10000."); _forwarderTimeout = value; } } public int ForwarderConcurrency { get { return _forwarderConcurrency; } set { if ((value < 1) || (value > 10)) throw new ArgumentOutOfRangeException(nameof(ForwarderConcurrency), "Valid range is from 1 to 10."); _forwarderConcurrency = value; } } public LogManager ResolverLogManager { get { return _resolverLog; } set { _resolverLog = value; } } public LogManager QueryLogManager { get { return _queryLog; } set { _queryLog = value; } } #endregion class CacheRefreshSample { public CacheRefreshSample(DnsQuestionRecord sampleQuestion, IReadOnlyList conditionalForwarders) { SampleQuestion = sampleQuestion; ConditionalForwarders = conditionalForwarders; } public DnsQuestionRecord SampleQuestion { get; } public IReadOnlyList ConditionalForwarders { get; } } class RecursiveResolveResponse { public RecursiveResolveResponse(DnsDatagram response, DnsDatagram checkingDisabledResponse) { Response = response; CheckingDisabledResponse = checkingDisabledResponse; } public DnsDatagram Response { get; } public DnsDatagram CheckingDisabledResponse { get; } } } #pragma warning restore CA2252 // This API requires opting into preview features #pragma warning restore CA1416 // Validate platform compatibility } ================================================ FILE: DnsServerCore/Dns/DnsServerException.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; namespace DnsServerCore.Dns { public class DnsServerException : Exception { #region constructors public DnsServerException() : base() { } public DnsServerException(string message) : base(message) { } public DnsServerException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore/Dns/Dnssec/DnssecEcdsaPrivateKey.cs ================================================ /* 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 System.IO; using System.Security.Cryptography; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns.Dnssec; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Dnssec { class DnssecEcdsaPrivateKey : DnssecPrivateKey { #region variables ECParameters _ecdsaPrivateKey; #endregion #region constructor public DnssecEcdsaPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, ECParameters ecdsaPrivateKey) : base(algorithm, keyType) { _ecdsaPrivateKey = ecdsaPrivateKey; InitDnsKey(); } public DnssecEcdsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version) : base(algorithm, bR, version) { InitDnsKey(); } #endregion #region private private void InitDnsKey() { ECParameters ecdsaPublicKey = new ECParameters { Curve = _ecdsaPrivateKey.Curve, Q = _ecdsaPrivateKey.Q }; InitDnsKey(new DnssecEcdsaPublicKey(ecdsaPublicKey)); } #endregion #region protected protected override byte[] SignHash(byte[] hash) { using (ECDsa ecdsa = ECDsa.Create(_ecdsaPrivateKey)) { return ecdsa.SignHash(hash, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); } } protected override void ReadPrivateKeyFrom(BinaryReader bR) { switch (Algorithm) { case DnssecAlgorithm.ECDSAP256SHA256: _ecdsaPrivateKey.Curve = ECCurve.NamedCurves.nistP256; break; case DnssecAlgorithm.ECDSAP384SHA384: _ecdsaPrivateKey.Curve = ECCurve.NamedCurves.nistP384; break; default: throw new InvalidDataException(); } _ecdsaPrivateKey.D = bR.ReadBuffer(); _ecdsaPrivateKey.Q.X = bR.ReadBuffer(); _ecdsaPrivateKey.Q.Y = bR.ReadBuffer(); } protected override void WritePrivateKeyTo(BinaryWriter bW) { bW.WriteBuffer(_ecdsaPrivateKey.D); bW.WriteBuffer(_ecdsaPrivateKey.Q.X); bW.WriteBuffer(_ecdsaPrivateKey.Q.Y); } #endregion } } ================================================ FILE: DnsServerCore/Dns/Dnssec/DnssecEddsaPrivateKey.cs ================================================ /* 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 Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; using System; using System.IO; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns.Dnssec; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Dnssec { class DnssecEddsaPrivateKey : DnssecPrivateKey { #region variables Ed25519PrivateKeyParameters _ed25519PrivateKey; Ed448PrivateKeyParameters _ed448PrivateKey; #endregion #region constructors public DnssecEddsaPrivateKey(DnssecPrivateKeyType keyType, Ed25519PrivateKeyParameters ed25519PrivateKey) : base(DnssecAlgorithm.ED25519, keyType) { _ed25519PrivateKey = ed25519PrivateKey; InitDnsKey(); } public DnssecEddsaPrivateKey(DnssecPrivateKeyType keyType, Ed448PrivateKeyParameters ed448PrivateKey) : base(DnssecAlgorithm.ED448, keyType) { _ed448PrivateKey = ed448PrivateKey; InitDnsKey(); } public DnssecEddsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version) : base(algorithm, bR, version) { InitDnsKey(); } #endregion #region private private void InitDnsKey() { switch (Algorithm) { case DnssecAlgorithm.ED25519: InitDnsKey(new DnssecEddsaPublicKey(_ed25519PrivateKey.GeneratePublicKey())); break; case DnssecAlgorithm.ED448: InitDnsKey(new DnssecEddsaPublicKey(_ed448PrivateKey.GeneratePublicKey())); break; } } #endregion #region protected protected override byte[] SignHash(byte[] hash) { ISigner signer; switch (Algorithm) { case DnssecAlgorithm.ED25519: signer = new Ed25519Signer(); signer.Init(true, _ed25519PrivateKey); break; case DnssecAlgorithm.ED448: signer = new Ed448Signer([]); signer.Init(true, _ed448PrivateKey); break; default: throw new InvalidOperationException(); } signer.BlockUpdate(hash); return signer.GenerateSignature(); } protected override void ReadPrivateKeyFrom(BinaryReader bR) { switch (Algorithm) { case DnssecAlgorithm.ED25519: _ed25519PrivateKey = new Ed25519PrivateKeyParameters(bR.ReadBuffer()); break; case DnssecAlgorithm.ED448: _ed448PrivateKey = new Ed448PrivateKeyParameters(bR.ReadBuffer()); break; default: throw new InvalidDataException(); } } protected override void WritePrivateKeyTo(BinaryWriter bW) { switch (Algorithm) { case DnssecAlgorithm.ED25519: bW.WriteBuffer(_ed25519PrivateKey.GetEncoded()); break; case DnssecAlgorithm.ED448: bW.WriteBuffer(_ed448PrivateKey.GetEncoded()); break; default: throw new InvalidDataException(); } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs ================================================ /* 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.Dns.Zones; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; using TechnitiumLibrary.Net.Dns.Dnssec; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Dnssec { //DNSSEC Key Rollover Timing Considerations //https://datatracker.ietf.org/doc/html/rfc7583 public enum DnssecPrivateKeyType : byte { Unknown = 0, KeySigningKey = 1, ZoneSigningKey = 2 } public enum DnssecPrivateKeyState : byte { Unknown = 0, /// /// Although keys may be created immediately prior to first /// use, some implementations may find it convenient to /// create a pool of keys in one operation and draw from it /// as required. (Note: such a pre-generated pool must be /// secured against surreptitious use.) In the timelines /// below, before the first event, the keys are considered to /// be created but not yet used: they are said to be in the /// "Generated" state. /// Generated = 1, /// /// A key enters the published state when either it or its associated data /// first appears in the appropriate zone. /// Published = 2, /// /// The DNSKEY or its associated data have been published for long enough /// to guarantee that copies of the key(s) it is replacing (or associated /// data related to that key) have expired from caches. /// Ready = 3, /// /// The data is starting to be used for validation. In the /// case of a ZSK, it means that the key is now being used to /// sign RRsets and that both it and the created RRSIGs /// appear in the zone. In the case of a KSK, it means that /// it is possible to use it to validate a DNSKEY RRset as /// both the DNSKEY and DS records are present in their /// respective zones. Note that when this state is entered, /// it may not be possible for validating resolvers to use /// the data for validation in all cases: the zone signing /// may not have finished or the data might not have reached /// the resolver because of propagation delays and/or caching /// issues. If this is the case, the resolver will have to /// rely on the predecessor data instead. /// Active = 4, /// /// The data has ceased to be used for validation. In the /// case of a ZSK, it means that the key is no longer used to /// sign RRsets. In the case of a KSK, it means that the /// successor DNSKEY and DS records are in place. In both /// cases, the key (and its associated data) can be removed /// as soon as it is safe to do so, i.e., when all validating /// resolvers are able to use the new key and associated data /// to validate the zone.However, until this happens, the /// current key and associated data must remain in their /// respective zones. /// Retired = 5, /// /// The key and its associated data are present in their /// respective zones, but there is no longer information /// anywhere that requires their presence for use in /// validation. Hence, they can be removed at any time. /// Dead = 6, /// /// Both the DNSKEY and its associated data have been removed /// from their respective zones. /// Removed = 7, /// /// The DNSKEY is published for a period with the "revoke" /// bit set as a way of notifying validating resolvers that /// have configured it as a trust anchor, as used in /// [RFC5011], that it is about to be removed from the zone. /// This state is used when [RFC5011] considerations are in /// effect (see Section 3.3.4). /// Revoked = 8 } public abstract class DnssecPrivateKey { #region variables readonly DnssecAlgorithm _algorithm; readonly DnssecPrivateKeyType _keyType; DnssecPrivateKeyState _state; DateTime _stateChangedOn; DateTime _stateTransitionBy; bool _isRetiring; ushort _rolloverDays; DnsDNSKEYRecordData _dnsKey; #endregion #region constructor protected DnssecPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType) { _algorithm = algorithm; _keyType = keyType; _state = DnssecPrivateKeyState.Generated; _stateChangedOn = DateTime.UtcNow; } protected DnssecPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version) { _algorithm = algorithm; _keyType = (DnssecPrivateKeyType)bR.ReadByte(); _state = (DnssecPrivateKeyState)bR.ReadByte(); _stateChangedOn = DateTime.UnixEpoch.AddSeconds(bR.ReadInt64()); if (version >= 2) _stateTransitionBy = DateTime.UnixEpoch.AddSeconds(bR.ReadInt64()); _isRetiring = bR.ReadBoolean(); _rolloverDays = bR.ReadUInt16(); ReadPrivateKeyFrom(bR); } #endregion #region static public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, int keySize = -1) { switch (algorithm) { case DnssecAlgorithm.RSAMD5: case DnssecAlgorithm.RSASHA1: case DnssecAlgorithm.RSASHA1_NSEC3_SHA1: case DnssecAlgorithm.RSASHA256: case DnssecAlgorithm.RSASHA512: if ((keySize < 1024) || (keySize > 4096)) throw new ArgumentOutOfRangeException(nameof(keySize), $"Valid RSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key size range is between 1024-4096 bits."); using (RSA rsa = RSA.Create(keySize)) { return new DnssecRsaPrivateKey(algorithm, keyType, keySize, rsa.ExportParameters(true)); } case DnssecAlgorithm.ECDSAP256SHA256: using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256)) { return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true)); } case DnssecAlgorithm.ECDSAP384SHA384: using (ECDsa ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384)) { return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true)); } case DnssecAlgorithm.ED25519: return new DnssecEddsaPrivateKey(keyType, new Ed25519PrivateKeyParameters(RandomNumberGenerator.GetBytes(32))); case DnssecAlgorithm.ED448: return new DnssecEddsaPrivateKey(keyType, new Ed448PrivateKeyParameters(RandomNumberGenerator.GetBytes(57))); default: throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString()); } } public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, string pemPrivateKey) { switch (algorithm) { case DnssecAlgorithm.RSAMD5: case DnssecAlgorithm.RSASHA1: case DnssecAlgorithm.RSASHA1_NSEC3_SHA1: case DnssecAlgorithm.RSASHA256: case DnssecAlgorithm.RSASHA512: using (RSA rsa = RSA.Create()) { rsa.ImportFromPem(pemPrivateKey); if ((rsa.KeySize < 1024) || (rsa.KeySize > 4096)) throw new ArgumentOutOfRangeException(nameof(pemPrivateKey), $"Valid RSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key size range is between 1024-4096 bits."); return new DnssecRsaPrivateKey(algorithm, keyType, rsa.KeySize, rsa.ExportParameters(true)); } case DnssecAlgorithm.ECDSAP256SHA256: using (ECDsa ecdsa = ECDsa.Create()) { ecdsa.ImportFromPem(pemPrivateKey); if (ecdsa.KeySize != 256) throw new ArgumentException($"The ECDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key must have key size of 256 bits.", nameof(pemPrivateKey)); return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true)); } case DnssecAlgorithm.ECDSAP384SHA384: using (ECDsa ecdsa = ECDsa.Create()) { ecdsa.ImportFromPem(pemPrivateKey); if (ecdsa.KeySize != 384) throw new ArgumentException($"The ECDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key must have key size of 384 bits.", nameof(pemPrivateKey)); return new DnssecEcdsaPrivateKey(algorithm, keyType, ecdsa.ExportParameters(true)); } case DnssecAlgorithm.ED25519: using (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) { if (pemReader.ReadObject() is not Ed25519PrivateKeyParameters privateKey) throw new ArgumentException($"The EdDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key must be for Ed25519 curve.", nameof(pemPrivateKey)); return new DnssecEddsaPrivateKey(keyType, privateKey); } case DnssecAlgorithm.ED448: using (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) { if (pemReader.ReadObject() is not Ed448PrivateKeyParameters privateKey) throw new ArgumentException($"The EdDSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key must be for Ed448 curve.", nameof(pemPrivateKey)); return new DnssecEddsaPrivateKey(keyType, privateKey); } default: throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString()); } } public static DnssecPrivateKey ReadFrom(BinaryReader bR) { if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DK") throw new InvalidDataException("DNSSEC private key format is invalid."); int version = bR.ReadByte(); switch (version) { case 1: case 2: DnssecAlgorithm algorithm = (DnssecAlgorithm)bR.ReadByte(); switch (algorithm) { case DnssecAlgorithm.RSAMD5: case DnssecAlgorithm.RSASHA1: case DnssecAlgorithm.RSASHA1_NSEC3_SHA1: case DnssecAlgorithm.RSASHA256: case DnssecAlgorithm.RSASHA512: return new DnssecRsaPrivateKey(algorithm, bR, version); case DnssecAlgorithm.ECDSAP256SHA256: case DnssecAlgorithm.ECDSAP384SHA384: return new DnssecEcdsaPrivateKey(algorithm, bR, version); case DnssecAlgorithm.ED25519: case DnssecAlgorithm.ED448: return new DnssecEddsaPrivateKey(algorithm, bR, version); default: throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString()); } default: throw new InvalidDataException("DNSSEC private key version not supported: " + version); } } #endregion #region protected protected void InitDnsKey(DnssecPublicKey publicKey) { DnsDnsKeyFlag flags = DnsDnsKeyFlag.ZoneKey; if (KeyType == DnssecPrivateKeyType.KeySigningKey) flags |= DnsDnsKeyFlag.SecureEntryPoint; if (_state == DnssecPrivateKeyState.Revoked) flags |= DnsDnsKeyFlag.Revoke; _dnsKey = new DnsDNSKEYRecordData(flags, 3, _algorithm, publicKey); } protected abstract byte[] SignHash(byte[] hash); protected abstract void ReadPrivateKeyFrom(BinaryReader bR); protected abstract void WritePrivateKeyTo(BinaryWriter bW); #endregion #region internal internal DnsResourceRecord SignRRSet(string signersName, IReadOnlyList records, uint signatureInceptionOffset, uint signatureValidityPeriod) { DnsResourceRecord firstRecord = records[0]; DnsRRSIGRecordData unsignedRRSigRecord = new DnsRRSIGRecordData(firstRecord.Type, _algorithm, DnsRRSIGRecordData.GetLabelCount(firstRecord.Name), firstRecord.OriginalTtlValue, Convert.ToUInt32((DateTime.UtcNow.AddSeconds(signatureValidityPeriod) - DateTime.UnixEpoch).TotalSeconds % uint.MaxValue), Convert.ToUInt32((DateTime.UtcNow.AddSeconds(-signatureInceptionOffset) - DateTime.UnixEpoch).TotalSeconds % uint.MaxValue), DnsKey.ComputedKeyTag, signersName, null); if (!DnsRRSIGRecordData.TryGetRRSetHash(unsignedRRSigRecord, records, out byte[] hash, out EDnsExtendedDnsErrorCode extendedDnsErrorCode)) throw new DnsServerException("Failed to sign record set: " + extendedDnsErrorCode.ToString()); byte[] signature = SignHash(hash); DnsRRSIGRecordData signedRRSigRecord = new DnsRRSIGRecordData(unsignedRRSigRecord.TypeCovered, unsignedRRSigRecord.Algorithm, unsignedRRSigRecord.Labels, unsignedRRSigRecord.OriginalTtl, unsignedRRSigRecord.SignatureExpiration, unsignedRRSigRecord.SignatureInception, unsignedRRSigRecord.KeyTag, unsignedRRSigRecord.SignersName, signature); return new DnsResourceRecord(firstRecord.Name, DnsResourceRecordType.RRSIG, firstRecord.Class, firstRecord.OriginalTtlValue, signedRRSigRecord); } internal void SetState(DnssecPrivateKeyState state, uint stateTransitionInTtl = 0) { if (_state >= state) return; //ignore; state cannot be updated to lower value _state = state; _stateChangedOn = DateTime.UtcNow; if (stateTransitionInTtl > 0) _stateTransitionBy = _stateChangedOn.AddSeconds(stateTransitionInTtl); else _stateTransitionBy = default; if (_state == DnssecPrivateKeyState.Revoked) InitDnsKey(_dnsKey.PublicKey); } internal void SetToRetire() { _isRetiring = true; } internal bool IsRolloverNeeded() { return (_rolloverDays > 0) && (DateTime.UtcNow > _stateChangedOn.AddDays(_rolloverDays)); } internal void WriteTo(BinaryWriter bW) { bW.Write(Encoding.ASCII.GetBytes("DK")); //format bW.Write((byte)2); //version bW.Write((byte)_algorithm); bW.Write((byte)_keyType); bW.Write((byte)_state); bW.Write(Convert.ToInt64((_stateChangedOn - DateTime.UnixEpoch).TotalSeconds)); bW.Write(Convert.ToInt64((_stateTransitionBy - DateTime.UnixEpoch).TotalSeconds)); bW.Write(_isRetiring); bW.Write(_rolloverDays); WritePrivateKeyTo(bW); } #endregion #region properties public DnssecAlgorithm Algorithm { get { return _algorithm; } } public DnssecPrivateKeyType KeyType { get { return _keyType; } } public DnssecPrivateKeyState State { get { return _state; } } public DateTime StateChangedOn { get { return _stateChangedOn; } } public DateTime StateTransitionBy { get { return _stateTransitionBy; } } public DateTime StateTransitionByWithDelays { get { return _stateTransitionBy.AddMilliseconds(PrimaryZone.DNSSEC_TIMER_PERIODIC_INTERVAL); } } public bool IsRetiring { get { return _isRetiring; } } public ushort RolloverDays { get { return _rolloverDays; } set { if (_keyType == DnssecPrivateKeyType.ZoneSigningKey) { if (value > 365) throw new ArgumentOutOfRangeException(nameof(RolloverDays), "Zone Signing Key (ZSK) automatic rollover days valid range is 0-365."); switch (_state) { case DnssecPrivateKeyState.Generated: case DnssecPrivateKeyState.Published: case DnssecPrivateKeyState.Ready: case DnssecPrivateKeyState.Active: if (_isRetiring) throw new InvalidOperationException("Zone Signing Key (ZSK) automatic rollover cannot be set since it is set to retire."); break; default: throw new InvalidOperationException("Zone Signing Key (ZSK) automatic rollover cannot be set due to invalid key state."); } } else { if (value != 0) throw new NotSupportedException("Automatic rollover is not supported for Key Signing Keys (KSK)."); } _rolloverDays = value; } } public DnsDNSKEYRecordData DnsKey { get { return _dnsKey; } } public ushort KeyTag { get { return _dnsKey.ComputedKeyTag; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Dnssec/DnssecRsaPrivateKey.cs ================================================ /* 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 System.IO; using System.Security.Cryptography; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns.Dnssec; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Dnssec { class DnssecRsaPrivateKey : DnssecPrivateKey { #region variables int _keySize; RSAParameters _rsaPrivateKey; readonly HashAlgorithmName _hashAlgorithm; #endregion #region constructor public DnssecRsaPrivateKey(DnssecAlgorithm algorithm, DnssecPrivateKeyType keyType, int keySize, RSAParameters rsaPrivateKey) : base(algorithm, keyType) { _keySize = keySize; _rsaPrivateKey = rsaPrivateKey; _hashAlgorithm = DnsRRSIGRecordData.GetHashAlgorithmName(algorithm); InitDnsKey(); } public DnssecRsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version) : base(algorithm, bR, version) { _hashAlgorithm = DnsRRSIGRecordData.GetHashAlgorithmName(algorithm); InitDnsKey(); } #endregion #region private private void InitDnsKey() { RSAParameters rsaPublicKey = new RSAParameters { Exponent = _rsaPrivateKey.Exponent, Modulus = _rsaPrivateKey.Modulus }; InitDnsKey(new DnssecRsaPublicKey(rsaPublicKey)); } #endregion #region protected protected override byte[] SignHash(byte[] hash) { using (RSA rsa = RSA.Create(_rsaPrivateKey)) { return rsa.SignHash(hash, _hashAlgorithm, RSASignaturePadding.Pkcs1); } } protected override void ReadPrivateKeyFrom(BinaryReader bR) { _keySize = bR.ReadInt32(); _rsaPrivateKey.D = bR.ReadBuffer(); _rsaPrivateKey.DP = bR.ReadBuffer(); _rsaPrivateKey.DQ = bR.ReadBuffer(); _rsaPrivateKey.Exponent = bR.ReadBuffer(); _rsaPrivateKey.InverseQ = bR.ReadBuffer(); _rsaPrivateKey.Modulus = bR.ReadBuffer(); _rsaPrivateKey.P = bR.ReadBuffer(); _rsaPrivateKey.Q = bR.ReadBuffer(); } protected override void WritePrivateKeyTo(BinaryWriter bW) { bW.Write(_keySize); bW.WriteBuffer(_rsaPrivateKey.D); bW.WriteBuffer(_rsaPrivateKey.DP); bW.WriteBuffer(_rsaPrivateKey.DQ); bW.WriteBuffer(_rsaPrivateKey.Exponent); bW.WriteBuffer(_rsaPrivateKey.InverseQ); bW.WriteBuffer(_rsaPrivateKey.Modulus); bW.WriteBuffer(_rsaPrivateKey.P); bW.WriteBuffer(_rsaPrivateKey.Q); } #endregion #region protected public int KeySize { get { return _keySize; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResolverDnsCache.cs ================================================ /* 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.ApplicationCommon; using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns { class ResolverDnsCache : IDnsCache { #region variables readonly DnsServer _dnsServer; readonly bool _skipDnsAppAuthoritativeRequestHandlers; readonly bool _skipConditionalForwardingResolution; #endregion #region constructor public ResolverDnsCache(DnsServer dnsServer, bool skipDnsAppAuthoritativeRequestHandlers, bool skipConditionalForwardingResolution = false) { _dnsServer = dnsServer; _skipDnsAppAuthoritativeRequestHandlers = skipDnsAppAuthoritativeRequestHandlers; _skipConditionalForwardingResolution = skipConditionalForwardingResolution; } #endregion #region private private async Task AuthoritativeQueryClosestDelegation(DnsDatagram request) { DnsDatagram authResponse = _dnsServer.AuthZoneManager.QueryClosestDelegation(request); DnsDatagram appResponse = await DnsApplicationQueryClosestDelegationAsync(request); if ((authResponse is not null) && (authResponse.Authority.Count > 0)) { if ((appResponse is not null) && (appResponse.Authority.Count > 0)) { DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord(); DnsResourceRecord appResponseFirstAuthority = appResponse.FindFirstAuthorityRecord(); if (appResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length) return appResponse; } return authResponse; } else { return appResponse; } } private async Task DnsApplicationQueryClosestDelegationAsync(DnsDatagram request) { if (_skipDnsAppAuthoritativeRequestHandlers || (_dnsServer.DnsApplicationManager.DnsAuthoritativeRequestHandlers.Count < 1) || (request.Question.Count != 1)) return null; IPEndPoint localEP = new IPEndPoint(IPAddress.Any, 0); DnsQuestionRecord question = request.Question[0]; string currentDomain = question.Name; while (true) { DnsDatagram nsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, true, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(currentDomain, DnsResourceRecordType.NS, DnsClass.IN) }); foreach (IDnsAuthoritativeRequestHandler requestHandler in _dnsServer.DnsApplicationManager.DnsAuthoritativeRequestHandlers) { try { DnsDatagram nsResponse = await requestHandler.ProcessRequestAsync(nsRequest, localEP, DnsTransportProtocol.Tcp, false); if (nsResponse is not null) { if ((nsResponse.Answer.Count > 0) && (nsResponse.Answer[0].Type == DnsResourceRecordType.NS)) return new DnsDatagram(request.Identifier, true, nsResponse.OPCODE, nsResponse.AuthoritativeAnswer, nsResponse.Truncation, nsResponse.RecursionDesired, nsResponse.RecursionAvailable, nsResponse.AuthenticData, nsResponse.CheckingDisabled, nsResponse.RCODE, request.Question, null, nsResponse.Answer, nsResponse.Additional); else if ((nsResponse.Authority.Count > 0) && (nsResponse.FindFirstAuthorityType() == DnsResourceRecordType.NS)) return new DnsDatagram(request.Identifier, true, nsResponse.OPCODE, nsResponse.AuthoritativeAnswer, nsResponse.Truncation, nsResponse.RecursionDesired, nsResponse.RecursionAvailable, nsResponse.AuthenticData, nsResponse.CheckingDisabled, nsResponse.RCODE, request.Question, null, nsResponse.Authority, nsResponse.Additional); } } catch (DnsClientException ex) { _dnsServer.ResolverLogManager?.Write(ex); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } //get parent domain int i = currentDomain.IndexOf('.'); if (i < 0) break; currentDomain = currentDomain.Substring(i + 1); } return null; } private Task DoConditionalForwardingResolutionAsync(DnsDatagram request, IReadOnlyList conditionalForwarders) { DnsQuestionRecord question = request.Question[0]; NetworkAddress eDnsClientSubnet = null; bool advancedForwardingClientSubnet = false; //this feature is used by Advanced Forwarding app to cache response per network group EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { //use ECS from client request switch (requestECS.Family) { case EDnsClientSubnetAddressFamily.IPv4: eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); break; case EDnsClientSubnetAddressFamily.IPv6: eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); break; } advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet; } ResolverDnsCache dnsCache = new ResolverDnsCache(_dnsServer, _skipDnsAppAuthoritativeRequestHandlers, true); return _dnsServer.PriorityConditionalForwarderResolveAsync(question, eDnsClientSubnet, advancedForwardingClientSubnet, dnsCache, _skipDnsAppAuthoritativeRequestHandlers, conditionalForwarders); } #endregion #region protected protected async Task QueryClosestDelegationAsync(DnsDatagram request) { DnsDatagram authResponse = await AuthoritativeQueryClosestDelegation(request); DnsDatagram cacheResponse = await _dnsServer.CacheZoneManager.QueryClosestDelegationAsync(request); if ((authResponse is not null) && (authResponse.Authority.Count > 0)) { if ((cacheResponse is not null) && (cacheResponse.Authority.Count > 0)) { DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord(); DnsResourceRecord cacheResponseFirstAuthority = cacheResponse.FindFirstAuthorityRecord(); if (cacheResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length) return cacheResponse; } return authResponse; } else { return cacheResponse; } } #endregion #region public public virtual async Task QueryAsync(DnsDatagram request, bool serveStale, bool findClosestNameServers = false, bool resetExpiry = false) { DnsDatagram authResponse = await _dnsServer.AuthoritativeQueryAsync(request, DnsTransportProtocol.Tcp, true, _skipDnsAppAuthoritativeRequestHandlers); if (authResponse is not null) { if ((authResponse.RCODE != DnsResponseCode.NoError) || (authResponse.Answer.Count > 0) || (authResponse.Authority.Count == 0) || authResponse.IsFirstAuthoritySOA()) return authResponse; } DnsDatagram cacheResponse = await _dnsServer.CacheZoneManager.QueryAsync(request, serveStale, findClosestNameServers, resetExpiry); if (cacheResponse is not null) { if ((cacheResponse.RCODE != DnsResponseCode.NoError) || (cacheResponse.Answer.Count > 0) || (cacheResponse.Authority.Count == 0) || cacheResponse.IsFirstAuthoritySOA()) return cacheResponse; } if ((authResponse is not null) && (authResponse.Authority.Count > 0)) { if ((cacheResponse is not null) && (cacheResponse.Authority.Count > 0)) { DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord(); DnsResourceRecord cacheResponseFirstAuthority = cacheResponse.FindFirstAuthorityRecord(); if (cacheResponseFirstAuthority.Name.Length > authResponseFirstAuthority.Name.Length) return cacheResponse; } if (!_skipConditionalForwardingResolution) { DnsResourceRecord authResponseFirstAuthority = authResponse.FindFirstAuthorityRecord(); if (authResponseFirstAuthority.Type == DnsResourceRecordType.FWD) return await DoConditionalForwardingResolutionAsync(request, authResponse.Authority); } return authResponse; } else { return cacheResponse; } } public void CacheResponse(DnsDatagram response, bool isDnssecBadCache = false, string zoneCut = null) { _dnsServer.CacheZoneManager.CacheResponse(response, isDnssecBadCache, zoneCut); } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResolverPrefetchDnsCache.cs ================================================ /* 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 System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.Dns { class ResolverPrefetchDnsCache : ResolverDnsCache { #region variables readonly DnsQuestionRecord _prefetchQuestion; #endregion #region constructor public ResolverPrefetchDnsCache(DnsServer dnsServer, bool skipDnsAppAuthoritativeRequestHandlers, DnsQuestionRecord prefetchQuestion) : base(dnsServer, skipDnsAppAuthoritativeRequestHandlers) { _prefetchQuestion = prefetchQuestion; } #endregion #region public public override Task QueryAsync(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false) { if (_prefetchQuestion.Equals(request.Question[0])) { //request is for prefetch question if (!findClosestNameServers) return Task.FromResult(null); //dont give answer from cache for prefetch question //return closest name servers so that the recursive resolver queries them to refreshes cache instead of returning response from cache return QueryClosestDelegationAsync(request); } return base.QueryAsync(request, serveStale, findClosestNameServers, resetExpiry); } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/AuthRecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using System.Net; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { abstract class AuthRecordInfo { #region constructor protected AuthRecordInfo() { } protected AuthRecordInfo(BinaryReader bR) { byte version = bR.ReadByte(); if (version >= 9) ReadRecordInfoFrom(bR); else ReadOldFormatFrom(bR, version, this is SOARecordInfo); } #endregion #region static public static GenericRecordInfo ReadGenericRecordInfoFrom(BinaryReader bR, DnsResourceRecordType type) { switch (type) { case DnsResourceRecordType.NS: return new NSRecordInfo(bR); case DnsResourceRecordType.SOA: return new SOARecordInfo(bR); case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: return new SVCBRecordInfo(bR); default: return new GenericRecordInfo(bR); } } #endregion #region private private void ReadOldFormatFrom(BinaryReader bR, byte version, bool isSoa) { switch (version) { case 1: { bool disabled = bR.ReadBoolean(); if (this is GenericRecordInfo info) info.Disabled = disabled; } break; case 2: case 3: case 4: case 5: case 6: case 7: case 8: { { bool disabled = bR.ReadBoolean(); if (this is GenericRecordInfo info) info.Disabled = disabled; } if ((version < 5) && isSoa) { //read old glue records as NameServerAddress in case of SOA record int count = bR.ReadByte(); if (count > 0) { NameServerAddress[] primaryNameServers = new NameServerAddress[count]; for (int i = 0; i < primaryNameServers.Length; i++) { DnsResourceRecord glueRecord = new DnsResourceRecord(bR.BaseStream); IPAddress address; switch (glueRecord.Type) { case DnsResourceRecordType.A: address = (glueRecord.RDATA as DnsARecordData).Address; break; case DnsResourceRecordType.AAAA: address = (glueRecord.RDATA as DnsAAAARecordData).Address; break; default: continue; } primaryNameServers[i] = new NameServerAddress(address); } (this as SOARecordInfo).PrimaryNameServers = primaryNameServers; } } else { int count = bR.ReadByte(); if (count > 0) { DnsResourceRecord[] glueRecords = new DnsResourceRecord[count]; for (int i = 0; i < glueRecords.Length; i++) glueRecords[i] = new DnsResourceRecord(bR.BaseStream); if (this is NSRecordInfo info) info.GlueRecords = glueRecords; } } if (version >= 3) { string comments = bR.ReadShortString(); if (this is GenericRecordInfo info) info.Comments = comments; } if (version >= 4) { DateTime deletedOn = bR.ReadDateTime(); if (this is HistoryRecordInfo info) info.DeletedOn = deletedOn; } if (version >= 5) { int count = bR.ReadByte(); if (count > 0) { NameServerAddress[] primaryNameServers = new NameServerAddress[count]; for (int i = 0; i < primaryNameServers.Length; i++) primaryNameServers[i] = new NameServerAddress(bR); if (this is SOARecordInfo info) info.PrimaryNameServers = primaryNameServers; } } if (version >= 7) { DnsTransportProtocol zoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte(); string tsigKeyName = bR.ReadShortString(); if (this is SOARecordInfo info) { if (zoneTransferProtocol != DnsTransportProtocol.Udp) info.ZoneTransferProtocol = zoneTransferProtocol; if (tsigKeyName.Length > 0) info.TsigKeyName = tsigKeyName; } } else if (version >= 6) { DnsTransportProtocol zoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte(); string tsigKeyName = bR.ReadShortString(); _ = bR.ReadShortString(); //_tsigSharedSecret (obsolete) _ = bR.ReadShortString(); //_tsigAlgorithm (obsolete) if (this is SOARecordInfo info) { if (zoneTransferProtocol != DnsTransportProtocol.Udp) info.ZoneTransferProtocol = zoneTransferProtocol; if (tsigKeyName.Length > 0) info.TsigKeyName = tsigKeyName; } } if (version >= 8) { bool useSoaSerialDateScheme = bR.ReadBoolean(); if (this is SOARecordInfo info) info.UseSoaSerialDateScheme = useSoaSerialDateScheme; } } break; default: throw new InvalidDataException("AuthRecordInfo format version not supported."); } } #endregion #region protected protected abstract void ReadRecordInfoFrom(BinaryReader bR); protected abstract void WriteRecordInfoTo(BinaryWriter bW); #endregion #region public public void WriteTo(BinaryWriter bW) { bW.Write((byte)9); //version WriteRecordInfoTo(bW); } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/CacheRecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.Collections.Generic; using System.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { class CacheRecordInfo { #region variables public static readonly CacheRecordInfo Default = new CacheRecordInfo(); IReadOnlyList _glueRecords; IReadOnlyList _rrsigRecords; IReadOnlyList _nsecRecords; NetworkAddress _eDnsClientSubnet; DnsDatagramMetadata _responseMetadata; DateTime _lastUsedOn; //not serialized #endregion #region constructor public CacheRecordInfo() { } public CacheRecordInfo(BinaryReader bR) { byte version = bR.ReadByte(); switch (version) { case 1: case 2: _glueRecords = ReadRecordsFrom(bR, true); _rrsigRecords = ReadRecordsFrom(bR, false); _nsecRecords = ReadRecordsFrom(bR, true); if (bR.ReadBoolean()) _eDnsClientSubnet = NetworkAddress.ReadFrom(bR); if (version >= 2) { if (bR.ReadBoolean()) _responseMetadata = new DnsDatagramMetadata(bR); } break; default: throw new InvalidDataException("CacheRecordInfo format version not supported."); } } #endregion #region private private static DnsResourceRecord[] ReadRecordsFrom(BinaryReader bR, bool includeInnerRRSigRecords) { int count = bR.ReadByte(); if (count == 0) return null; DnsResourceRecord[] records = new DnsResourceRecord[count]; for (int i = 0; i < count; i++) { records[i] = DnsResourceRecord.ReadCacheRecordFrom(bR, delegate (DnsResourceRecord record) { if (includeInnerRRSigRecords) { IReadOnlyList rrsigRecords = ReadRecordsFrom(bR, false); if (rrsigRecords is not null) record.GetCacheRecordInfo()._rrsigRecords = rrsigRecords; } }); } return records; } private static void WriteRecordsTo(IReadOnlyList records, BinaryWriter bW, bool includeInnerRRSigRecords) { if (records is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(records.Count)); foreach (DnsResourceRecord record in records) { record.WriteCacheRecordTo(bW, delegate () { if (includeInnerRRSigRecords) { if (record.Tag is CacheRecordInfo cacheRecordInfo) WriteRecordsTo(cacheRecordInfo._rrsigRecords, bW, false); else bW.Write((byte)0); } }); } } } #endregion #region public public void WriteTo(BinaryWriter bW) { bW.Write((byte)2); //version WriteRecordsTo(_glueRecords, bW, true); WriteRecordsTo(_rrsigRecords, bW, false); WriteRecordsTo(_nsecRecords, bW, true); if (_eDnsClientSubnet is null) { bW.Write(false); } else { bW.Write(true); _eDnsClientSubnet.WriteTo(bW); } if (_responseMetadata is null) { bW.Write(false); } else { bW.Write(true); _responseMetadata.WriteTo(bW); } } #endregion #region properties public IReadOnlyList GlueRecords { get { return _glueRecords; } set { if ((value is null) || (value.Count == 0)) _glueRecords = null; else _glueRecords = value; } } public IReadOnlyList RRSIGRecords { get { return _rrsigRecords; } set { if ((value is null) || (value.Count == 0)) _rrsigRecords = null; else _rrsigRecords = value; } } public IReadOnlyList NSECRecords { get { return _nsecRecords; } set { if ((value is null) || (value.Count == 0)) _nsecRecords = null; else _nsecRecords = value; } } public NetworkAddress EDnsClientSubnet { get { return _eDnsClientSubnet; } set { _eDnsClientSubnet = value; } } public DnsDatagramMetadata ResponseMetadata { get { return _responseMetadata; } set { _responseMetadata = value; } } public DateTime LastUsedOn { get { return _lastUsedOn; } set { _lastUsedOn = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/DnsNSRecordDataExtended.cs ================================================ /* 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 TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { class DnsNSRecordDataExtended : DnsNSRecordData { #region constructors public DnsNSRecordDataExtended(string nameServer, bool validateName = true) : base(nameServer, validateName) { } #endregion #region public public void UpdateNameServer(string nameServer) { _nameServer = nameServer; } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/DnsResourceRecordExtensions.cs ================================================ /* 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 System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { static class DnsResourceRecordExtensions { public static void SetGlueRecords(this DnsResourceRecord record, string glueAddresses) { if (record.RDATA is not DnsNSRecordData nsRecord) throw new InvalidOperationException(); string domain = nsRecord.NameServer; IPAddress[] glueAddressesList = glueAddresses.Split(IPAddress.Parse, ','); DnsResourceRecord[] glueRecords = new DnsResourceRecord[glueAddressesList.Length]; for (int i = 0; i < glueRecords.Length; i++) { switch (glueAddressesList[i].AddressFamily) { case AddressFamily.InterNetwork: glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.A, DnsClass.IN, record.TTL, new DnsARecordData(glueAddressesList[i])); break; case AddressFamily.InterNetworkV6: glueRecords[i] = new DnsResourceRecord(domain, DnsResourceRecordType.AAAA, DnsClass.IN, record.TTL, new DnsAAAARecordData(glueAddressesList[i])); break; } } record.GetAuthNSRecordInfo().GlueRecords = glueRecords; } public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyList allGlueRecords) { if (record.RDATA is not DnsNSRecordData nsRecord) throw new InvalidOperationException(); string domain = nsRecord.NameServer; List foundGlueRecords = new List(2); foreach (DnsResourceRecord glueRecord in allGlueRecords) { switch (glueRecord.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (glueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) foundGlueRecords.Add(glueRecord); break; } } record.GetAuthNSRecordInfo().GlueRecords = foundGlueRecords; } public static void SyncGlueRecords(this DnsResourceRecord record, IReadOnlyCollection deletedGlueRecords, IReadOnlyCollection addedGlueRecords) { if (record.RDATA is not DnsNSRecordData nsRecord) throw new InvalidOperationException(); bool updated = false; List updatedGlueRecords = new List(); IReadOnlyList existingGlueRecords = record.GetAuthNSRecordInfo().GlueRecords; if (existingGlueRecords is not null) { foreach (DnsResourceRecord existingGlueRecord in existingGlueRecords) { if (deletedGlueRecords.Contains(existingGlueRecord)) updated = true; //skipped to delete existing glue record else updatedGlueRecords.Add(existingGlueRecord); } } string domain = nsRecord.NameServer; foreach (DnsResourceRecord addedGlueRecord in addedGlueRecords) { switch (addedGlueRecord.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (addedGlueRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) { updatedGlueRecords.Add(addedGlueRecord); updated = true; } break; } } if (updated) record.GetAuthNSRecordInfo().GlueRecords = updatedGlueRecords; } public static GenericRecordInfo GetAuthGenericRecordInfo(this DnsResourceRecord record) { if (record.Tag is null) { GenericRecordInfo rrInfo; switch (record.Type) { case DnsResourceRecordType.NS: rrInfo = new NSRecordInfo(); break; case DnsResourceRecordType.SOA: rrInfo = new SOARecordInfo(); break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: rrInfo = new SVCBRecordInfo(); break; default: rrInfo = new GenericRecordInfo(); break; } record.Tag = rrInfo; return rrInfo; } else if (record.Tag is GenericRecordInfo rrInfo) { return rrInfo; } else { throw new InvalidOperationException(); } } public static NSRecordInfo GetAuthNSRecordInfo(this DnsResourceRecord record) { if (record.Tag is null) { NSRecordInfo info = new NSRecordInfo(); record.Tag = info; return info; } else if (record.Tag is NSRecordInfo nsInfo) { return nsInfo; } else { throw new InvalidOperationException(); } } public static SOARecordInfo GetAuthSOARecordInfo(this DnsResourceRecord record) { if (record.Tag is null) { SOARecordInfo info = new SOARecordInfo(); record.Tag = info; return info; } else if (record.Tag is SOARecordInfo soaInfo) { return soaInfo; } else { throw new InvalidOperationException(); } } public static SVCBRecordInfo GetAuthSVCBRecordInfo(this DnsResourceRecord record) { if (record.Tag is null) { SVCBRecordInfo info = new SVCBRecordInfo(); record.Tag = info; return info; } else if (record.Tag is SVCBRecordInfo svcbInfo) { return svcbInfo; } else { throw new InvalidOperationException(); } } public static HistoryRecordInfo GetAuthHistoryRecordInfo(this DnsResourceRecord record) { if (record.Tag is null) { HistoryRecordInfo info = new HistoryRecordInfo(); record.Tag = info; return info; } else if (record.Tag is HistoryRecordInfo info) { return info; } else { throw new InvalidOperationException(); } } public static CacheRecordInfo GetCacheRecordInfo(this DnsResourceRecord record) { if (record.Tag is not CacheRecordInfo rrInfo) { rrInfo = new CacheRecordInfo(); record.Tag = rrInfo; } return rrInfo; } public static void CopyRecordInfoFrom(this DnsResourceRecord record, DnsResourceRecord otherRecord) { record.Tag = otherRecord.Tag; } } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/DnsSOARecordDataExtended.cs ================================================ /* 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 TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { class DnsSOARecordDataExtended : DnsSOARecordData { #region constructor public DnsSOARecordDataExtended(string primaryNameServer, string responsiblePerson, uint serial, uint refresh, uint retry, uint expire, uint minimum) : base(primaryNameServer, responsiblePerson, serial, refresh, retry, expire, minimum) { } #endregion #region public public void UpdatePrimaryNameServerAndMinimum(string primaryNameServer, uint minimum) { _primaryNameServer = primaryNameServer; _minimum = minimum; } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/GenericRecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Dns.ResourceRecords { class GenericRecordInfo : AuthRecordInfo { #region variables bool _disabled; string _comments; DateTime _lastModified; uint _expiryTtl; DateTime _lastUsedOn; //not serialized #endregion #region constructor public GenericRecordInfo() { } public GenericRecordInfo(BinaryReader bR) : base(bR) { } #endregion #region protected protected sealed override void ReadRecordInfoFrom(BinaryReader bR) { byte version = bR.ReadByte(); switch (version) { case 1: _disabled = bR.ReadBoolean(); _comments = bR.ReadShortString(); ReadExtendedRecordInfoFrom(bR); break; case 2: _disabled = bR.ReadBoolean(); _comments = bR.ReadShortString(); _lastModified = bR.ReadDateTime(); _expiryTtl = bR.ReadUInt32(); ReadExtendedRecordInfoFrom(bR); break; default: throw new InvalidDataException("GenericRecordInfo format version not supported."); } } protected sealed override void WriteRecordInfoTo(BinaryWriter bW) { bW.Write((byte)2); //version bW.Write(_disabled); if (string.IsNullOrEmpty(_comments)) bW.Write((byte)0); else bW.WriteShortString(_comments); bW.Write(_lastModified); bW.Write(_expiryTtl); WriteExtendedRecordInfoTo(bW); } protected virtual void ReadExtendedRecordInfoFrom(BinaryReader bR) { _ = bR.ReadByte(); //read byte to move ahead } protected virtual void WriteExtendedRecordInfoTo(BinaryWriter bW) { bW.Write((byte)0); //no extended info } #endregion #region public public uint GetPendingExpiryTtl() { uint elapsedSeconds = Convert.ToUInt32((DateTime.UtcNow - _lastModified).TotalSeconds); if (elapsedSeconds < _expiryTtl) return _expiryTtl - elapsedSeconds; return 0u; } #endregion #region properties public virtual bool Disabled { get { return _disabled; } set { _disabled = value; } } public string Comments { get { return _comments; } set { if ((value is not null) && (value.Length > 255)) throw new ArgumentOutOfRangeException(nameof(Comments), "Resource record comment text cannot exceed 255 characters."); _comments = value; } } public DateTime LastModified { get { return _lastModified; } set { _lastModified = value; } } public virtual uint ExpiryTtl { get { return _expiryTtl; } set { _expiryTtl = value; } } public DateTime LastUsedOn { get { return _lastUsedOn; } set { _lastUsedOn = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/HistoryRecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.IO; using TechnitiumLibrary.IO; namespace DnsServerCore.Dns.ResourceRecords { class HistoryRecordInfo : AuthRecordInfo { #region variables DateTime _deletedOn; #endregion #region constructor public HistoryRecordInfo() { } public HistoryRecordInfo(BinaryReader bR) : base(bR) { } #endregion #region static public static HistoryRecordInfo ReadFrom(BinaryReader bR) { return new HistoryRecordInfo(bR); } #endregion #region protected protected override void ReadRecordInfoFrom(BinaryReader bR) { byte version = bR.ReadByte(); switch (version) { case 1: _deletedOn = bR.ReadDateTime(); break; default: throw new InvalidDataException("HistoryRecordInfo format version not supported."); } } protected override void WriteRecordInfoTo(BinaryWriter bW) { bW.Write((byte)1); //version bW.Write(_deletedOn); } #endregion #region properties public DateTime DeletedOn { get { return _deletedOn; } set { _deletedOn = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/NSRecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.Collections.Generic; using System.IO; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ResourceRecords { class NSRecordInfo : GenericRecordInfo { #region variables IReadOnlyList _glueRecords; #endregion #region constructor public NSRecordInfo() { } public NSRecordInfo(BinaryReader bR) : base(bR) { } #endregion #region protected protected override void ReadExtendedRecordInfoFrom(BinaryReader bR) { byte version = bR.ReadByte(); switch (version) { case 0: //no extended info break; case 1: int count = bR.ReadByte(); if (count > 0) { DnsResourceRecord[] glueRecords = new DnsResourceRecord[count]; for (int i = 0; i < glueRecords.Length; i++) glueRecords[i] = new DnsResourceRecord(bR.BaseStream); _glueRecords = glueRecords; } break; default: throw new InvalidDataException("NSRecordInfo format version not supported."); } } protected override void WriteExtendedRecordInfoTo(BinaryWriter bW) { bW.Write((byte)1); //version if (_glueRecords is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(_glueRecords.Count)); foreach (DnsResourceRecord glueRecord in _glueRecords) glueRecord.WriteTo(bW.BaseStream); } } #endregion #region properties public IReadOnlyList GlueRecords { get { return _glueRecords; } set { if ((value is null) || (value.Count == 0)) _glueRecords = null; else _glueRecords = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/SOARecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using System.IO; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.Dns.ResourceRecords { class SOARecordInfo : GenericRecordInfo { #region variables byte _version; bool _useSoaSerialDateScheme; IReadOnlyList _primaryNameServers; //depricated DnsTransportProtocol _zoneTransferProtocol; //depricated string _tsigKeyName = string.Empty; //depricated #endregion #region constructor public SOARecordInfo() { } public SOARecordInfo(BinaryReader bR) : base(bR) { } #endregion #region protected protected override void ReadExtendedRecordInfoFrom(BinaryReader bR) { _version = bR.ReadByte(); switch (_version) { case 0: //no extended info break; case 1: int count = bR.ReadByte(); if (count > 0) { NameServerAddress[] primaryNameServers = new NameServerAddress[count]; for (int i = 0; i < primaryNameServers.Length; i++) primaryNameServers[i] = new NameServerAddress(bR); _primaryNameServers = primaryNameServers; } _zoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte(); _tsigKeyName = bR.ReadShortString(); _useSoaSerialDateScheme = bR.ReadBoolean(); break; case 2: _useSoaSerialDateScheme = bR.ReadBoolean(); break; default: throw new InvalidDataException("SOARecordInfo format version not supported."); } } protected override void WriteExtendedRecordInfoTo(BinaryWriter bW) { bW.Write((byte)2); //version bW.Write(_useSoaSerialDateScheme); } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { //cannot disable SOA } } public override uint ExpiryTtl { get { return base.ExpiryTtl; } set { //cannot expire SOA } } public byte Version { get { return _version; } } public bool UseSoaSerialDateScheme { get { return _useSoaSerialDateScheme; } set { _useSoaSerialDateScheme = value; } } public IReadOnlyList PrimaryNameServers { get { return _primaryNameServers; } set { _primaryNameServers = value; } } public DnsTransportProtocol ZoneTransferProtocol { get { return _zoneTransferProtocol; } set { _zoneTransferProtocol = value; } } public string TsigKeyName { get { return _tsigKeyName; } set { _tsigKeyName = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ResourceRecords/SVCBRecordInfo.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.IO; namespace DnsServerCore.Dns.ResourceRecords { class SVCBRecordInfo : GenericRecordInfo { #region variables bool _autoIpv4Hint; bool _autoIpv6Hint; #endregion #region constructor public SVCBRecordInfo() { } public SVCBRecordInfo(BinaryReader bR) : base(bR) { } #endregion #region protected protected override void ReadExtendedRecordInfoFrom(BinaryReader bR) { byte version = bR.ReadByte(); switch (version) { case 0: //no extended info break; case 1: _autoIpv4Hint = bR.ReadBoolean(); _autoIpv6Hint = bR.ReadBoolean(); break; default: throw new InvalidDataException("SVCBRecordInfo format version not supported."); } } protected override void WriteExtendedRecordInfoTo(BinaryWriter bW) { bW.Write((byte)1); //version bW.Write(_autoIpv4Hint); bW.Write(_autoIpv6Hint); } #endregion #region properties public bool AutoIpv4Hint { get { return _autoIpv4Hint; } set { _autoIpv4Hint = value; } } public bool AutoIpv6Hint { get { return _autoIpv6Hint; } set { _autoIpv6Hint = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/StatsManager.cs ================================================ /* 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.ApplicationCommon; using DnsServerCore.HttpApi.Models; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Channels; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns { public sealed class StatsManager : IDisposable { #region variables const int DAILY_STATS_FILE_TOP_LIMIT = 1000; readonly static HourlyStats _emptyHourlyStats = new HourlyStats(); readonly static StatCounter _emptyDailyStats = new StatCounter(); readonly DnsServer _dnsServer; readonly string _statsFolder; readonly StatCounter[] _lastHourStatCounters = new StatCounter[60]; readonly StatCounter[] _lastHourStatCountersCopy = new StatCounter[60]; readonly ConcurrentDictionary _hourlyStatsCache = new ConcurrentDictionary(); readonly ConcurrentDictionary _dailyStatsCache = new ConcurrentDictionary(); readonly Timer _maintenanceTimer; const int MAINTENANCE_TIMER_INITIAL_INTERVAL = 10000; const int MAINTENANCE_TIMER_PERIODIC_INTERVAL = 10000; readonly Channel _channel; readonly ChannelWriter _channelWriter; readonly Thread _consumerThread; readonly Timer _statsCleanupTimer; const int STATS_CLEANUP_TIMER_INITIAL_INTERVAL = 60 * 1000; const int STATS_CLEANUP_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000; bool _enableInMemoryStats; int _maxStatFileDays; #endregion #region constructor static StatsManager() { _emptyDailyStats.Lock(); } public StatsManager(DnsServer dnsServer) { _dnsServer = dnsServer; _statsFolder = Path.Combine(dnsServer.ConfigFolder, "stats"); if (!Directory.Exists(_statsFolder)) Directory.CreateDirectory(_statsFolder); UnboundedChannelOptions options = new UnboundedChannelOptions(); options.SingleReader = true; _channel = Channel.CreateUnbounded(options); _channelWriter = _channel.Writer; //load stats LoadLastHourStats(); try { //do first maintenance DoMaintenance(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } //start periodic maintenance timer _maintenanceTimer = new Timer(delegate (object state) { try { DoMaintenance(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }, null, MAINTENANCE_TIMER_INITIAL_INTERVAL, MAINTENANCE_TIMER_PERIODIC_INTERVAL); //stats consumer thread _consumerThread = new Thread(async delegate () { try { await foreach (StatsQueueItem item in _channel.Reader.ReadAllAsync()) { if (_disposed) break; StatCounter statCounter = _lastHourStatCounters[item._timestamp.Minute]; if (statCounter is not null) { DnsQuestionRecord query; if ((item._request is not null) && (item._request.Question.Count > 0)) query = item._request.Question[0]; else query = null; DnsServerResponseType responseType; if (item._response is null) responseType = DnsServerResponseType.Dropped; else if (item._response.Tag is null) responseType = DnsServerResponseType.Recursive; else responseType = (DnsServerResponseType)item._response.Tag; statCounter.Update(query, item._response is null ? DnsResponseCode.NoError : item._response.RCODE, responseType, item._remoteEP.Address, item._protocol, item._rateLimited); } if ((item._request is null) || (item._response is null)) continue; //skip dropped requests for apps to prevent DoS foreach (IDnsQueryLogger logger in _dnsServer.DnsApplicationManager.DnsQueryLoggers) { try { _ = logger.InsertLogAsync(item._timestamp, item._request, item._remoteEP, item._protocol, item._response); } catch (Exception ex) { dnsServer.LogManager.Write(ex); } } } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }); _consumerThread.Name = "Stats"; _consumerThread.IsBackground = true; _consumerThread.Start(); _statsCleanupTimer = new Timer(delegate (object state) { try { if (_maxStatFileDays < 1) return; DateTime cutoffDate = DateTime.UtcNow.AddDays(_maxStatFileDays * -1).Date; //delete hourly logs { string[] hourlyStatsFiles = Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, "stats"), "*.stat"); foreach (string hourlyStatsFile in hourlyStatsFiles) { string hourlyStatsFileName = Path.GetFileNameWithoutExtension(hourlyStatsFile); if (!DateTime.TryParseExact(hourlyStatsFileName, "yyyyMMddHH", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime hourlyStatsFileDate)) continue; if (hourlyStatsFileDate < cutoffDate) { try { File.Delete(hourlyStatsFile); dnsServer.LogManager.Write("StatsManager cleanup deleted the hourly stats file: " + hourlyStatsFile); } catch (Exception ex) { dnsServer.LogManager.Write(ex); } } } } //delete daily logs { string[] dailyStatsFiles = Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, "stats"), "*.dstat"); foreach (string dailyStatsFile in dailyStatsFiles) { string dailyStatsFileName = Path.GetFileNameWithoutExtension(dailyStatsFile); if (!DateTime.TryParseExact(dailyStatsFileName, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime dailyStatsFileDate)) continue; if (dailyStatsFileDate < cutoffDate) { try { File.Delete(dailyStatsFile); dnsServer.LogManager.Write("StatsManager cleanup deleted the daily stats file: " + dailyStatsFile); } catch (Exception ex) { dnsServer.LogManager.Write(ex); } } } } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }); _statsCleanupTimer.Change(STATS_CLEANUP_TIMER_INITIAL_INTERVAL, STATS_CLEANUP_TIMER_PERIODIC_INTERVAL); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _maintenanceTimer?.Dispose(); _statsCleanupTimer?.Dispose(); _channelWriter?.TryComplete(); DoMaintenance(); //do last maintenance _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private void LoadLastHourStats() { try { DateTime currentDateTime = DateTime.UtcNow; DateTime lastHourDateTime = currentDateTime.AddMinutes(-60); HourlyStats lastHourlyStats = null; DateTime lastHourlyStatsDateTime = new DateTime(); for (int i = 0; i < 60; i++) { DateTime lastDateTime = lastHourDateTime.AddMinutes(i); if ((lastHourlyStats == null) || (lastDateTime.Hour != lastHourlyStatsDateTime.Hour)) { lastHourlyStats = LoadHourlyStats(lastDateTime); lastHourlyStatsDateTime = lastDateTime; } _lastHourStatCounters[lastDateTime.Minute] = lastHourlyStats.MinuteStats[lastDateTime.Minute]; _lastHourStatCountersCopy[lastDateTime.Minute] = _lastHourStatCounters[lastDateTime.Minute]; } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } private void DoMaintenance() { //load new stats counter 5 min ahead of current time DateTime currentDateTime = DateTime.UtcNow; for (int i = 0; i < 5; i++) { int minute = currentDateTime.AddMinutes(i).Minute; StatCounter statCounter = _lastHourStatCounters[minute]; if ((statCounter == null) || statCounter.IsLocked) _lastHourStatCounters[minute] = new StatCounter(); } //save data upto last 5 mins DateTime last5MinDateTime = currentDateTime.AddMinutes(-5); for (int i = 0; i < 5; i++) { DateTime lastDateTime = last5MinDateTime.AddMinutes(i); StatCounter lastStatCounter = _lastHourStatCounters[lastDateTime.Minute]; if ((lastStatCounter != null) && !lastStatCounter.IsLocked) { lastStatCounter.Lock(); if (!_enableInMemoryStats) { //load hourly stats data HourlyStats hourlyStats = LoadHourlyStats(lastDateTime); //update hourly stats file hourlyStats.UpdateStat(lastDateTime, lastStatCounter); //save hourly stats SaveHourlyStats(lastDateTime, hourlyStats); } //keep copy for api _lastHourStatCountersCopy[lastDateTime.Minute] = lastStatCounter; } } //load previous day stats to auto create daily stats file LoadDailyStats(currentDateTime.AddDays(-1)); //remove old data from hourly stats cache { DateTime threshold = DateTime.UtcNow.AddHours(-24); threshold = new DateTime(threshold.Year, threshold.Month, threshold.Day, threshold.Hour, 0, 0, DateTimeKind.Utc); List _keysToRemove = new List(); foreach (KeyValuePair item in _hourlyStatsCache) { if (item.Key < threshold) _keysToRemove.Add(item.Key); } foreach (DateTime key in _keysToRemove) _hourlyStatsCache.TryRemove(key, out _); } //unload minute stats data from hourly stats cache for data older than last hour { DateTime lastHourThreshold = DateTime.UtcNow.AddHours(-1); lastHourThreshold = new DateTime(lastHourThreshold.Year, lastHourThreshold.Month, lastHourThreshold.Day, lastHourThreshold.Hour, 0, 0, DateTimeKind.Utc); foreach (KeyValuePair item in _hourlyStatsCache) { if (item.Key < lastHourThreshold) item.Value.UnloadMinuteStats(); } } //remove old data from daily stats cache { DateTime threshold = DateTime.UtcNow.AddMonths(-12); threshold = new DateTime(threshold.Year, threshold.Month, 1, 0, 0, 0, DateTimeKind.Utc); List _keysToRemove = new List(); foreach (KeyValuePair item in _dailyStatsCache) { if (item.Key < threshold) _keysToRemove.Add(item.Key); } foreach (DateTime key in _keysToRemove) _dailyStatsCache.TryRemove(key, out _); } } private HourlyStats LoadHourlyStats(DateTime dateTime, bool forceReload = false, bool ifNotExistsReturnEmptyHourlyStats = false) { if (_enableInMemoryStats) return _emptyHourlyStats; DateTime hourlyDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, 0, 0, 0, DateTimeKind.Utc); if (forceReload || !_hourlyStatsCache.TryGetValue(hourlyDateTime, out HourlyStats hourlyStats)) { string hourlyStatsFile = Path.Combine(_statsFolder, dateTime.ToString("yyyyMMddHH") + ".stat"); if (File.Exists(hourlyStatsFile)) { try { using (FileStream fS = new FileStream(hourlyStatsFile, FileMode.Open, FileAccess.Read)) { hourlyStats = new HourlyStats(new BinaryReader(fS)); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); if (ifNotExistsReturnEmptyHourlyStats) hourlyStats = _emptyHourlyStats; else hourlyStats = new HourlyStats(); } } else { if (ifNotExistsReturnEmptyHourlyStats) hourlyStats = _emptyHourlyStats; else hourlyStats = new HourlyStats(); } _hourlyStatsCache[hourlyDateTime] = hourlyStats; } return hourlyStats; } private StatCounter LoadDailyStats(DateTime dateTime) { if (_enableInMemoryStats) return _emptyDailyStats; DateTime dailyDateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 0, 0, 0, 0, DateTimeKind.Utc); if (!_dailyStatsCache.TryGetValue(dailyDateTime, out StatCounter dailyStats)) { string dailyStatsFile = Path.Combine(_statsFolder, dateTime.ToString("yyyyMMdd") + ".dstat"); if (File.Exists(dailyStatsFile)) { try { using (FileStream fS = new FileStream(dailyStatsFile, FileMode.Open, FileAccess.Read)) { dailyStats = new StatCounter(new BinaryReader(fS)); } //check if existing file could be truncated to avoid loading unnecessary data in memory if (dailyStats.Truncate(DAILY_STATS_FILE_TOP_LIMIT)) { SaveDailyStats(dailyDateTime, dailyStats); //save truncated file GC.Collect(); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } if (dailyStats is null) { dailyStats = new StatCounter(); dailyStats.Lock(); for (int hour = 0; hour < 24; hour++) //hours { HourlyStats hourlyStats = LoadHourlyStats(dailyDateTime.AddHours(hour), ifNotExistsReturnEmptyHourlyStats: true); dailyStats.Merge(hourlyStats.HourStat); } if (dailyStats.TotalQueries > 0) { _ = dailyStats.Truncate(DAILY_STATS_FILE_TOP_LIMIT); SaveDailyStats(dailyDateTime, dailyStats); GC.Collect(); } } if (!_dailyStatsCache.TryAdd(dailyDateTime, dailyStats)) { if (!_dailyStatsCache.TryGetValue(dailyDateTime, out dailyStats)) throw new DnsServerException("Unable to load daily stats."); } } return dailyStats; } private void SaveHourlyStats(DateTime dateTime, HourlyStats hourlyStats) { string hourlyStatsFile = Path.Combine(_statsFolder, dateTime.ToString("yyyyMMddHH") + ".stat"); try { using (FileStream fS = new FileStream(hourlyStatsFile, FileMode.Create, FileAccess.Write)) { hourlyStats.WriteTo(new BinaryWriter(fS)); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } private void SaveDailyStats(DateTime dateTime, StatCounter dailyStats) { string dailyStatsFile = Path.Combine(_statsFolder, dateTime.ToString("yyyyMMdd") + ".dstat"); try { using (FileStream fS = new FileStream(dailyStatsFile, FileMode.Create, FileAccess.Write)) { dailyStats.WriteTo(new BinaryWriter(fS)); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } private void Flush() { //clear in memory stats for (int i = 0; i < _lastHourStatCountersCopy.Length; i++) _lastHourStatCountersCopy[i] = null; _hourlyStatsCache.Clear(); _dailyStatsCache.Clear(); } #endregion #region public public void ReloadStats() { Flush(); LoadLastHourStats(); } public void DeleteAllStats() { foreach (string hourlyStatsFile in Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, "stats"), "*.stat", SearchOption.TopDirectoryOnly)) { File.Delete(hourlyStatsFile); } foreach (string dailyStatsFile in Directory.GetFiles(Path.Combine(_dnsServer.ConfigFolder, "stats"), "*.dstat", SearchOption.TopDirectoryOnly)) { File.Delete(dailyStatsFile); } Flush(); } public void QueueUpdate(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response, bool rateLimited) { _channelWriter.TryWrite(new StatsQueueItem(request, remoteEP, protocol, response, rateLimited)); } public DashboardStats GetLastHourMinuteWiseStats(bool utcFormat) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); string[] labels = new string[60]; long[] totalQueriesPerInterval = new long[60]; long[] totalNoErrorPerInterval = new long[60]; long[] totalServerFailurePerInterval = new long[60]; long[] totalNxDomainPerInterval = new long[60]; long[] totalRefusedPerInterval = new long[60]; long[] totalAuthHitPerInterval = new long[60]; long[] totalRecursionsPerInterval = new long[60]; long[] totalCacheHitPerInterval = new long[60]; long[] totalBlockedPerInterval = new long[60]; long[] totalDroppedPerInterval = new long[60]; long[] totalClientsPerInterval = new long[60]; DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(-60); lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc); for (int minute = 0; minute < 60; minute++) { DateTime lastDateTime = lastHourDateTime.AddMinutes(minute); string label; if (utcFormat) label = lastDateTime.AddMinutes(1).ToString("O"); else label = lastDateTime.AddMinutes(1).ToLocalTime().ToString("HH:mm"); labels[minute] = label; StatCounter statCounter = _lastHourStatCountersCopy[lastDateTime.Minute]; if ((statCounter != null) && statCounter.IsLocked) { totalStatCounter.Merge(statCounter); totalQueriesPerInterval[minute] = statCounter.TotalQueries; totalNoErrorPerInterval[minute] = statCounter.TotalNoError; totalServerFailurePerInterval[minute] = statCounter.TotalServerFailure; totalNxDomainPerInterval[minute] = statCounter.TotalNxDomain; totalRefusedPerInterval[minute] = statCounter.TotalRefused; totalAuthHitPerInterval[minute] = statCounter.TotalAuthoritative; totalRecursionsPerInterval[minute] = statCounter.TotalRecursive; totalCacheHitPerInterval[minute] = statCounter.TotalCached; totalBlockedPerInterval[minute] = statCounter.TotalBlocked; totalDroppedPerInterval[minute] = statCounter.TotalDropped; totalClientsPerInterval[minute] = statCounter.TotalClients; } } DashboardStats.ChartData mainChartData = new DashboardStats.ChartData() { Labels = labels, DataSets = [ new DashboardStats.DataSet() { Label = "Total", Data = totalQueriesPerInterval }, new DashboardStats.DataSet() { Label = "No Error", Data = totalNoErrorPerInterval }, new DashboardStats.DataSet() { Label = "Server Failure", Data = totalServerFailurePerInterval }, new DashboardStats.DataSet() { Label = "NX Domain", Data = totalNxDomainPerInterval }, new DashboardStats.DataSet() { Label = "Refused", Data = totalRefusedPerInterval }, new DashboardStats.DataSet() { Label = "Authoritative", Data = totalAuthHitPerInterval }, new DashboardStats.DataSet() { Label = "Recursive", Data = totalRecursionsPerInterval }, new DashboardStats.DataSet() { Label = "Cached", Data = totalCacheHitPerInterval }, new DashboardStats.DataSet() { Label = "Blocked", Data = totalBlockedPerInterval }, new DashboardStats.DataSet() { Label = "Dropped", Data = totalDroppedPerInterval }, new DashboardStats.DataSet() { Label = "Clients", Data = totalClientsPerInterval } ] }; return new DashboardStats() { Stats = totalStatCounter.GetStatsData(), MainChartData = mainChartData, QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(), QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(), ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(), TopClients = totalStatCounter.GetTopClientStats(10), TopDomains = totalStatCounter.GetTopDomainStats(10), TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10) }; } public DashboardStats GetLastDayHourWiseStats(bool utcFormat) { return GetHourWiseStats(DateTime.UtcNow.AddHours(-24), 24, utcFormat); } public DashboardStats GetLastWeekDayWiseStats(bool utcFormat) { return GetDayWiseStats(DateTime.UtcNow.AddDays(-7).Date, 7, utcFormat); } public DashboardStats GetLastMonthDayWiseStats(bool utcFormat) { return GetDayWiseStats(DateTime.UtcNow.AddDays(-31).Date, 31, utcFormat); } public DashboardStats GetLastYearMonthWiseStats(bool utcFormat) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); string[] labels = new string[12]; long[] totalQueriesPerInterval = new long[12]; long[] totalNoErrorPerInterval = new long[12]; long[] totalServerFailurePerInterval = new long[12]; long[] totalNxDomainPerInterval = new long[12]; long[] totalRefusedPerInterval = new long[12]; long[] totalAuthHitPerInterval = new long[12]; long[] totalRecursionsPerInterval = new long[12]; long[] totalCacheHitPerInterval = new long[12]; long[] totalBlockedPerInterval = new long[12]; long[] totalDroppedPerInterval = new long[12]; long[] totalClientsPerInterval = new long[12]; DateTime lastYearDateTime = DateTime.UtcNow.AddMonths(-12); lastYearDateTime = new DateTime(lastYearDateTime.Year, lastYearDateTime.Month, 1, 0, 0, 0, DateTimeKind.Utc); for (int month = 0; month < 12; month++) //months { StatCounter monthlyStatCounter = new StatCounter(); monthlyStatCounter.Lock(); DateTime lastMonthDateTime = lastYearDateTime.AddMonths(month); string label; if (utcFormat) label = lastMonthDateTime.ToString("O"); else label = lastMonthDateTime.ToLocalTime().ToString("MM/yyyy"); labels[month] = label; int days = DateTime.DaysInMonth(lastMonthDateTime.Year, lastMonthDateTime.Month); for (int day = 0; day < days; day++) //days { StatCounter dailyStatCounter = LoadDailyStats(lastMonthDateTime.AddDays(day)); monthlyStatCounter.Merge(dailyStatCounter, true); } totalStatCounter.Merge(monthlyStatCounter, true); totalQueriesPerInterval[month] = monthlyStatCounter.TotalQueries; totalNoErrorPerInterval[month] = monthlyStatCounter.TotalNoError; totalServerFailurePerInterval[month] = monthlyStatCounter.TotalServerFailure; totalNxDomainPerInterval[month] = monthlyStatCounter.TotalNxDomain; totalRefusedPerInterval[month] = monthlyStatCounter.TotalRefused; totalAuthHitPerInterval[month] = monthlyStatCounter.TotalAuthoritative; totalRecursionsPerInterval[month] = monthlyStatCounter.TotalRecursive; totalCacheHitPerInterval[month] = monthlyStatCounter.TotalCached; totalBlockedPerInterval[month] = monthlyStatCounter.TotalBlocked; totalDroppedPerInterval[month] = monthlyStatCounter.TotalDropped; totalClientsPerInterval[month] = monthlyStatCounter.TotalClients; } DashboardStats.ChartData mainChartData = new DashboardStats.ChartData() { Labels = labels, DataSets = [ new DashboardStats.DataSet() { Label = "Total", Data = totalQueriesPerInterval }, new DashboardStats.DataSet() { Label = "No Error", Data = totalNoErrorPerInterval }, new DashboardStats.DataSet() { Label = "Server Failure", Data = totalServerFailurePerInterval }, new DashboardStats.DataSet() { Label = "NX Domain", Data = totalNxDomainPerInterval }, new DashboardStats.DataSet() { Label = "Refused", Data = totalRefusedPerInterval }, new DashboardStats.DataSet() { Label = "Authoritative", Data = totalAuthHitPerInterval }, new DashboardStats.DataSet() { Label = "Recursive", Data = totalRecursionsPerInterval }, new DashboardStats.DataSet() { Label = "Cached", Data = totalCacheHitPerInterval }, new DashboardStats.DataSet() { Label = "Blocked", Data = totalBlockedPerInterval }, new DashboardStats.DataSet() { Label = "Dropped", Data = totalDroppedPerInterval }, new DashboardStats.DataSet() { Label = "Clients", Data = totalClientsPerInterval } ] }; return new DashboardStats() { Stats = totalStatCounter.GetStatsData(), MainChartData = mainChartData, QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(), QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(), ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(), TopClients = totalStatCounter.GetTopClientStats(10), TopDomains = totalStatCounter.GetTopDomainStats(10), TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10) }; } public DashboardStats GetMinuteWiseStats(DateTime startDate, DateTime endDate, bool utcFormat) { return GetMinuteWiseStats(startDate, Convert.ToInt32((endDate - startDate).TotalMinutes) + 1, utcFormat); } public DashboardStats GetMinuteWiseStats(DateTime startDate, int minutes, bool utcFormat) { startDate = startDate.AddMinutes(-1); StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); string[] labels = new string[minutes]; long[] totalQueriesPerInterval = new long[minutes]; long[] totalNoErrorPerInterval = new long[minutes]; long[] totalServerFailurePerInterval = new long[minutes]; long[] totalNxDomainPerInterval = new long[minutes]; long[] totalRefusedPerInterval = new long[minutes]; long[] totalAuthHitPerInterval = new long[minutes]; long[] totalRecursionsPerInterval = new long[minutes]; long[] totalCacheHitPerInterval = new long[minutes]; long[] totalBlockedPerInterval = new long[minutes]; long[] totalDroppedPerInterval = new long[minutes]; long[] totalClientsPerInterval = new long[minutes]; for (int minute = 0; minute < minutes; minute++) { DateTime lastDateTime = startDate.AddMinutes(minute); HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true); if (hourlyStats.MinuteStats is null) hourlyStats = LoadHourlyStats(lastDateTime, true); StatCounter minuteStatCounter = hourlyStats.MinuteStats[lastDateTime.Minute]; string label; if (utcFormat) label = lastDateTime.AddMinutes(1).ToString("O"); else label = lastDateTime.AddMinutes(1).ToLocalTime().ToString("MM/dd HH:mm"); labels[minute] = label; totalStatCounter.Merge(minuteStatCounter); totalQueriesPerInterval[minute] = minuteStatCounter.TotalQueries; totalNoErrorPerInterval[minute] = minuteStatCounter.TotalNoError; totalServerFailurePerInterval[minute] = minuteStatCounter.TotalServerFailure; totalNxDomainPerInterval[minute] = minuteStatCounter.TotalNxDomain; totalRefusedPerInterval[minute] = minuteStatCounter.TotalRefused; totalAuthHitPerInterval[minute] = minuteStatCounter.TotalAuthoritative; totalRecursionsPerInterval[minute] = minuteStatCounter.TotalRecursive; totalCacheHitPerInterval[minute] = minuteStatCounter.TotalCached; totalBlockedPerInterval[minute] = minuteStatCounter.TotalBlocked; totalDroppedPerInterval[minute] = minuteStatCounter.TotalDropped; totalClientsPerInterval[minute] = minuteStatCounter.TotalClients; } DashboardStats.ChartData mainChartData = new DashboardStats.ChartData() { Labels = labels, DataSets = [ new DashboardStats.DataSet() { Label = "Total", Data = totalQueriesPerInterval }, new DashboardStats.DataSet() { Label = "No Error", Data = totalNoErrorPerInterval }, new DashboardStats.DataSet() { Label = "Server Failure", Data = totalServerFailurePerInterval }, new DashboardStats.DataSet() { Label = "NX Domain", Data = totalNxDomainPerInterval }, new DashboardStats.DataSet() { Label = "Refused", Data = totalRefusedPerInterval }, new DashboardStats.DataSet() { Label = "Authoritative", Data = totalAuthHitPerInterval }, new DashboardStats.DataSet() { Label = "Recursive", Data = totalRecursionsPerInterval }, new DashboardStats.DataSet() { Label = "Cached", Data = totalCacheHitPerInterval }, new DashboardStats.DataSet() { Label = "Blocked", Data = totalBlockedPerInterval }, new DashboardStats.DataSet() { Label = "Dropped", Data = totalDroppedPerInterval }, new DashboardStats.DataSet() { Label = "Clients", Data = totalClientsPerInterval } ] }; return new DashboardStats() { Stats = totalStatCounter.GetStatsData(), MainChartData = mainChartData, QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(), QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(), ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(), TopClients = totalStatCounter.GetTopClientStats(10), TopDomains = totalStatCounter.GetTopDomainStats(10), TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10) }; } public DashboardStats GetHourWiseStats(DateTime startDate, DateTime endDate, bool utcFormat) { return GetHourWiseStats(startDate, Convert.ToInt32((endDate - startDate).TotalHours) + 1, utcFormat); } public DashboardStats GetHourWiseStats(DateTime startDate, int hours, bool utcFormat) { startDate = new DateTime(startDate.Year, startDate.Month, startDate.Day, startDate.Hour, 0, 0, 0, DateTimeKind.Utc); StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); string[] labels = new string[hours]; long[] totalQueriesPerInterval = new long[hours]; long[] totalNoErrorPerInterval = new long[hours]; long[] totalServerFailurePerInterval = new long[hours]; long[] totalNxDomainPerInterval = new long[hours]; long[] totalRefusedPerInterval = new long[hours]; long[] totalAuthHitPerInterval = new long[hours]; long[] totalRecursionsPerInterval = new long[hours]; long[] totalCacheHitPerInterval = new long[hours]; long[] totalBlockedPerInterval = new long[hours]; long[] totalDroppedPerInterval = new long[hours]; long[] totalClientsPerInterval = new long[hours]; for (int hour = 0; hour < hours; hour++) { DateTime lastDateTime = startDate.AddHours(hour); string label; if (utcFormat) label = lastDateTime.AddHours(1).ToString("O"); else label = lastDateTime.AddHours(1).ToLocalTime().ToString("MM/dd HH") + ":00"; labels[hour] = label; HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true); StatCounter hourlyStatCounter = hourlyStats.HourStat; totalStatCounter.Merge(hourlyStatCounter); totalQueriesPerInterval[hour] = hourlyStatCounter.TotalQueries; totalNoErrorPerInterval[hour] = hourlyStatCounter.TotalNoError; totalServerFailurePerInterval[hour] = hourlyStatCounter.TotalServerFailure; totalNxDomainPerInterval[hour] = hourlyStatCounter.TotalNxDomain; totalRefusedPerInterval[hour] = hourlyStatCounter.TotalRefused; totalAuthHitPerInterval[hour] = hourlyStatCounter.TotalAuthoritative; totalRecursionsPerInterval[hour] = hourlyStatCounter.TotalRecursive; totalCacheHitPerInterval[hour] = hourlyStatCounter.TotalCached; totalBlockedPerInterval[hour] = hourlyStatCounter.TotalBlocked; totalDroppedPerInterval[hour] = hourlyStatCounter.TotalDropped; totalClientsPerInterval[hour] = hourlyStatCounter.TotalClients; } DashboardStats.ChartData mainChartData = new DashboardStats.ChartData() { Labels = labels, DataSets = [ new DashboardStats.DataSet() { Label = "Total", Data = totalQueriesPerInterval }, new DashboardStats.DataSet() { Label = "No Error", Data = totalNoErrorPerInterval }, new DashboardStats.DataSet() { Label = "Server Failure", Data = totalServerFailurePerInterval }, new DashboardStats.DataSet() { Label = "NX Domain", Data = totalNxDomainPerInterval }, new DashboardStats.DataSet() { Label = "Refused", Data = totalRefusedPerInterval }, new DashboardStats.DataSet() { Label = "Authoritative", Data = totalAuthHitPerInterval }, new DashboardStats.DataSet() { Label = "Recursive", Data = totalRecursionsPerInterval }, new DashboardStats.DataSet() { Label = "Cached", Data = totalCacheHitPerInterval }, new DashboardStats.DataSet() { Label = "Blocked", Data = totalBlockedPerInterval }, new DashboardStats.DataSet() { Label = "Dropped", Data = totalDroppedPerInterval }, new DashboardStats.DataSet() { Label = "Clients", Data = totalClientsPerInterval } ] }; return new DashboardStats() { Stats = totalStatCounter.GetStatsData(), MainChartData = mainChartData, QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(), QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(), ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(), TopClients = totalStatCounter.GetTopClientStats(10), TopDomains = totalStatCounter.GetTopDomainStats(10), TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10) }; } public DashboardStats GetDayWiseStats(DateTime startDate, DateTime endDate, bool utcFormat) { return GetDayWiseStats(startDate, Convert.ToInt32((endDate - startDate).TotalDays) + 1, utcFormat); } public DashboardStats GetDayWiseStats(DateTime startDate, int days, bool utcFormat) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); string[] labels = new string[days]; long[] totalQueriesPerInterval = new long[days]; long[] totalNoErrorPerInterval = new long[days]; long[] totalServerFailurePerInterval = new long[days]; long[] totalNxDomainPerInterval = new long[days]; long[] totalRefusedPerInterval = new long[days]; long[] totalAuthHitPerInterval = new long[days]; long[] totalRecursionsPerInterval = new long[days]; long[] totalCacheHitPerInterval = new long[days]; long[] totalBlockedPerInterval = new long[days]; long[] totalDroppedPerInterval = new long[days]; long[] totalClientsPerInterval = new long[days]; for (int day = 0; day < days; day++) //days { DateTime lastDayDateTime = startDate.AddDays(day); string label; if (utcFormat) label = lastDayDateTime.ToString("O"); else label = lastDayDateTime.ToLocalTime().ToString("MM/dd"); labels[day] = label; StatCounter dailyStatCounter = LoadDailyStats(lastDayDateTime); totalStatCounter.Merge(dailyStatCounter, true); totalQueriesPerInterval[day] = dailyStatCounter.TotalQueries; totalNoErrorPerInterval[day] = dailyStatCounter.TotalNoError; totalServerFailurePerInterval[day] = dailyStatCounter.TotalServerFailure; totalNxDomainPerInterval[day] = dailyStatCounter.TotalNxDomain; totalRefusedPerInterval[day] = dailyStatCounter.TotalRefused; totalAuthHitPerInterval[day] = dailyStatCounter.TotalAuthoritative; totalRecursionsPerInterval[day] = dailyStatCounter.TotalRecursive; totalCacheHitPerInterval[day] = dailyStatCounter.TotalCached; totalBlockedPerInterval[day] = dailyStatCounter.TotalBlocked; totalDroppedPerInterval[day] = dailyStatCounter.TotalDropped; totalClientsPerInterval[day] = dailyStatCounter.TotalClients; } DashboardStats.ChartData mainChartData = new DashboardStats.ChartData() { Labels = labels, DataSets = [ new DashboardStats.DataSet() { Label = "Total", Data = totalQueriesPerInterval }, new DashboardStats.DataSet() { Label = "No Error", Data = totalNoErrorPerInterval }, new DashboardStats.DataSet() { Label = "Server Failure", Data = totalServerFailurePerInterval }, new DashboardStats.DataSet() { Label = "NX Domain", Data = totalNxDomainPerInterval }, new DashboardStats.DataSet() { Label = "Refused", Data = totalRefusedPerInterval }, new DashboardStats.DataSet() { Label = "Authoritative", Data = totalAuthHitPerInterval }, new DashboardStats.DataSet() { Label = "Recursive", Data = totalRecursionsPerInterval }, new DashboardStats.DataSet() { Label = "Cached", Data = totalCacheHitPerInterval }, new DashboardStats.DataSet() { Label = "Blocked", Data = totalBlockedPerInterval }, new DashboardStats.DataSet() { Label = "Dropped", Data = totalDroppedPerInterval }, new DashboardStats.DataSet() { Label = "Clients", Data = totalClientsPerInterval } ] }; return new DashboardStats() { Stats = totalStatCounter.GetStatsData(), MainChartData = mainChartData, QueryResponseChartData = totalStatCounter.GetQueryResponseChartData(), QueryTypeChartData = totalStatCounter.GetTopQueryTypesChartData(), ProtocolTypeChartData = totalStatCounter.GetTopProtocolTypesChartData(), TopClients = totalStatCounter.GetTopClientStats(10), TopDomains = totalStatCounter.GetTopDomainStats(10), TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(10) }; } public DashboardStats GetLastHourTopStats(DashboardTopStatsType type, int limit) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(-60); lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc); for (int minute = 0; minute < 60; minute++) { DateTime lastDateTime = lastHourDateTime.AddMinutes(minute); StatCounter statCounter = _lastHourStatCountersCopy[lastDateTime.Minute]; if ((statCounter != null) && statCounter.IsLocked) totalStatCounter.Merge(statCounter); } switch (type) { case DashboardTopStatsType.TopClients: return new DashboardStats() { TopClients = totalStatCounter.GetTopClientStats(limit), }; case DashboardTopStatsType.TopDomains: return new DashboardStats() { TopDomains = totalStatCounter.GetTopDomainStats(limit), }; case DashboardTopStatsType.TopBlockedDomains: return new DashboardStats() { TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit) }; default: throw new NotSupportedException(); } } public DashboardStats GetLastDayTopStats(DashboardTopStatsType type, int limit) { return GetHourWiseTopStats(DateTime.UtcNow.AddHours(-24), 24, type, limit); } public DashboardStats GetLastWeekTopStats(DashboardTopStatsType type, int limit) { return GetDayWiseTopStats(DateTime.UtcNow.AddDays(-7).Date, 7, type, limit); } public DashboardStats GetLastMonthTopStats(DashboardTopStatsType type, int limit) { return GetDayWiseTopStats(DateTime.UtcNow.AddDays(-31).Date, 31, type, limit); } public DashboardStats GetLastYearTopStats(DashboardTopStatsType type, int limit) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); DateTime lastYearDateTime = DateTime.UtcNow.AddMonths(-12); lastYearDateTime = new DateTime(lastYearDateTime.Year, lastYearDateTime.Month, 1, 0, 0, 0, DateTimeKind.Utc); for (int month = 0; month < 12; month++) //months { StatCounter monthlyStatCounter = new StatCounter(); monthlyStatCounter.Lock(); DateTime lastMonthDateTime = lastYearDateTime.AddMonths(month); int days = DateTime.DaysInMonth(lastMonthDateTime.Year, lastMonthDateTime.Month); for (int day = 0; day < days; day++) //days { StatCounter dailyStatCounter = LoadDailyStats(lastMonthDateTime.AddDays(day)); monthlyStatCounter.Merge(dailyStatCounter, true); } totalStatCounter.Merge(monthlyStatCounter, true); } switch (type) { case DashboardTopStatsType.TopClients: return new DashboardStats() { TopClients = totalStatCounter.GetTopClientStats(limit), }; case DashboardTopStatsType.TopDomains: return new DashboardStats() { TopDomains = totalStatCounter.GetTopDomainStats(limit), }; case DashboardTopStatsType.TopBlockedDomains: return new DashboardStats() { TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit) }; default: throw new NotSupportedException(); } } public DashboardStats GetMinuteWiseTopStats(DateTime startDate, DateTime endDate, DashboardTopStatsType type, int limit) { return GetMinuteWiseTopStats(startDate, Convert.ToInt32((endDate - startDate).TotalMinutes) + 1, type, limit); } public DashboardStats GetMinuteWiseTopStats(DateTime startDate, int minutes, DashboardTopStatsType type, int limit) { startDate = startDate.AddMinutes(-1); StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); for (int minute = 0; minute < minutes; minute++) { DateTime lastDateTime = startDate.AddMinutes(minute); HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true); if (hourlyStats.MinuteStats is null) hourlyStats = LoadHourlyStats(lastDateTime, true); StatCounter minuteStatCounter = hourlyStats.MinuteStats[lastDateTime.Minute]; totalStatCounter.Merge(minuteStatCounter); } switch (type) { case DashboardTopStatsType.TopClients: return new DashboardStats() { TopClients = totalStatCounter.GetTopClientStats(limit), }; case DashboardTopStatsType.TopDomains: return new DashboardStats() { TopDomains = totalStatCounter.GetTopDomainStats(limit), }; case DashboardTopStatsType.TopBlockedDomains: return new DashboardStats() { TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit) }; default: throw new NotSupportedException(); } } public DashboardStats GetHourWiseTopStats(DateTime startDate, DateTime endDate, DashboardTopStatsType type, int limit) { return GetHourWiseTopStats(startDate, Convert.ToInt32((endDate - startDate).TotalHours) + 1, type, limit); } public DashboardStats GetHourWiseTopStats(DateTime startDate, int hours, DashboardTopStatsType type, int limit) { startDate = new DateTime(startDate.Year, startDate.Month, startDate.Day, startDate.Hour, 0, 0, 0, DateTimeKind.Utc); StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); for (int hour = 0; hour < hours; hour++) { DateTime lastDateTime = startDate.AddHours(hour); HourlyStats hourlyStats = LoadHourlyStats(lastDateTime, ifNotExistsReturnEmptyHourlyStats: true); StatCounter hourlyStatCounter = hourlyStats.HourStat; totalStatCounter.Merge(hourlyStatCounter); } switch (type) { case DashboardTopStatsType.TopClients: return new DashboardStats() { TopClients = totalStatCounter.GetTopClientStats(limit), }; case DashboardTopStatsType.TopDomains: return new DashboardStats() { TopDomains = totalStatCounter.GetTopDomainStats(limit), }; case DashboardTopStatsType.TopBlockedDomains: return new DashboardStats() { TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit) }; default: throw new NotSupportedException(); } } public DashboardStats GetDayWiseTopStats(DateTime startDate, DateTime endDate, DashboardTopStatsType type, int limit) { return GetDayWiseTopStats(startDate, Convert.ToInt32((endDate - startDate).TotalDays) + 1, type, limit); } public DashboardStats GetDayWiseTopStats(DateTime startDate, int days, DashboardTopStatsType type, int limit) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); for (int day = 0; day < days; day++) //days { DateTime lastDayDateTime = startDate.AddDays(day); StatCounter dailyStatCounter = LoadDailyStats(lastDayDateTime); totalStatCounter.Merge(dailyStatCounter, true); } switch (type) { case DashboardTopStatsType.TopClients: return new DashboardStats() { TopClients = totalStatCounter.GetTopClientStats(limit), }; case DashboardTopStatsType.TopDomains: return new DashboardStats() { TopDomains = totalStatCounter.GetTopDomainStats(limit), }; case DashboardTopStatsType.TopBlockedDomains: return new DashboardStats() { TopBlockedDomains = totalStatCounter.GetTopBlockedDomainStats(limit) }; default: throw new NotSupportedException(); } } public List> GetLastHourEligibleQueries(int minimumHitsPerHour) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(-60); lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc); for (int minute = 0; minute < 60; minute++) { DateTime lastDateTime = lastHourDateTime.AddMinutes(minute); StatCounter statCounter = _lastHourStatCountersCopy[lastDateTime.Minute]; if ((statCounter != null) && statCounter.IsLocked) totalStatCounter.Merge(statCounter); } return totalStatCounter.GetEligibleQueries(minimumHitsPerHour); } public Dictionary> GetLatestClientSubnetStats(int minutes, IEnumerable ipv4Prefixes, IEnumerable ipv6Prefixes) { StatCounter totalStatCounter = new StatCounter(); totalStatCounter.Lock(); DateTime lastHourDateTime = DateTime.UtcNow.AddMinutes(1 - minutes); lastHourDateTime = new DateTime(lastHourDateTime.Year, lastHourDateTime.Month, lastHourDateTime.Day, lastHourDateTime.Hour, lastHourDateTime.Minute, 0, DateTimeKind.Utc); for (int minute = 0; minute < minutes; minute++) { DateTime lastDateTime = lastHourDateTime.AddMinutes(minute); StatCounter statCounter = _lastHourStatCounters[lastDateTime.Minute]; if (statCounter is not null) totalStatCounter.Merge(statCounter, false, true); } return totalStatCounter.GetClientSubnetStats(ipv4Prefixes, ipv6Prefixes); } #endregion #region properties public bool EnableInMemoryStats { get { return _enableInMemoryStats; } set { if (_enableInMemoryStats != value) { _enableInMemoryStats = value; if (_enableInMemoryStats) { _hourlyStatsCache.Clear(); _dailyStatsCache.Clear(); } } } } public int MaxStatFileDays { get { return _maxStatFileDays; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(MaxStatFileDays), "MaxStatFileDays must be greater than or equal to 0."); _maxStatFileDays = value; if (_maxStatFileDays == 0) _statsCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); else _statsCleanupTimer.Change(STATS_CLEANUP_TIMER_INITIAL_INTERVAL, STATS_CLEANUP_TIMER_PERIODIC_INTERVAL); } } #endregion class HourlyStats { #region variables readonly StatCounter _hourStat; //calculated value StatCounter[] _minuteStats = new StatCounter[60]; #endregion #region constructor public HourlyStats() { _hourStat = new StatCounter(); _hourStat.Lock(); for (int i = 0; i < _minuteStats.Length; i++) { _minuteStats[i] = new StatCounter(); _minuteStats[i].Lock(); } } public HourlyStats(BinaryReader bR) { if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "HS") //format throw new InvalidDataException("HourlyStats format is invalid."); byte version = bR.ReadByte(); switch (version) { case 1: _hourStat = new StatCounter(); _hourStat.Lock(); for (int i = 0; i < _minuteStats.Length; i++) { _minuteStats[i] = new StatCounter(bR); _hourStat.Merge(_minuteStats[i]); } break; default: throw new InvalidDataException("HourlyStats version not supported."); } } #endregion #region public public void UpdateStat(DateTime dateTime, StatCounter minuteStat) { if (!minuteStat.IsLocked) throw new DnsServerException("StatCounter must be locked."); _hourStat.Merge(minuteStat); _minuteStats[dateTime.Minute] = minuteStat; } public void UnloadMinuteStats() { _minuteStats = null; } public void WriteTo(BinaryWriter bW) { bW.Write(Encoding.ASCII.GetBytes("HS")); //format bW.Write((byte)1); //version for (int i = 0; i < _minuteStats.Length; i++) { if (_minuteStats[i] == null) { _minuteStats[i] = new StatCounter(); _minuteStats[i].Lock(); } _minuteStats[i].WriteTo(bW); } } #endregion #region properties public StatCounter HourStat { get { return _hourStat; } } public StatCounter[] MinuteStats { get { return _minuteStats; } } #endregion } class StatCounter { #region variables volatile bool _locked; long _totalQueries; long _totalNoError; long _totalServerFailure; long _totalNxDomain; long _totalRefused; long _totalAuthoritative; long _totalRecursive; long _totalCached; long _totalBlocked; long _totalDropped; long _totalClients; readonly ConcurrentDictionary _queryDomains; readonly ConcurrentDictionary _queryBlockedDomains; readonly ConcurrentDictionary _queryTypes; readonly ConcurrentDictionary _protocolTypes; readonly ConcurrentDictionary _clientIpAddressesUdpTcp; readonly ConcurrentDictionary _queries; bool _truncationFoundDuringMerge; long _totalClientsDailyStatsSummation; #endregion #region constructor public StatCounter() { _queryDomains = new ConcurrentDictionary(); _queryBlockedDomains = new ConcurrentDictionary(); _queryTypes = new ConcurrentDictionary(); _protocolTypes = new ConcurrentDictionary(); _clientIpAddressesUdpTcp = new ConcurrentDictionary(); _queries = new ConcurrentDictionary(); } public StatCounter(BinaryReader bR) { if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "SC") //format throw new InvalidDataException("StatCounter format is invalid."); byte version = bR.ReadByte(); switch (version) { case 1: case 2: case 3: case 4: case 5: case 6: _totalQueries = bR.ReadInt32(); _totalNoError = bR.ReadInt32(); _totalServerFailure = bR.ReadInt32(); _totalNxDomain = bR.ReadInt32(); _totalRefused = bR.ReadInt32(); if (version >= 3) { _totalAuthoritative = bR.ReadInt32(); _totalRecursive = bR.ReadInt32(); _totalCached = bR.ReadInt32(); _totalBlocked = bR.ReadInt32(); } else { _totalBlocked = bR.ReadInt32(); if (version >= 2) _totalCached = bR.ReadInt32(); } if (version >= 6) _totalClients = bR.ReadInt32(); { int count = bR.ReadInt32(); _queryDomains = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queryDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt32())); } { int count = bR.ReadInt32(); _queryBlockedDomains = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queryBlockedDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt32())); } { int count = bR.ReadInt32(); _queryTypes = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queryTypes.TryAdd((DnsResourceRecordType)bR.ReadUInt16(), new Counter(bR.ReadInt32())); } _protocolTypes = new ConcurrentDictionary(1, 0); { int count = bR.ReadInt32(); _clientIpAddressesUdpTcp = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _clientIpAddressesUdpTcp.TryAdd(IPAddressExtensions.ReadFrom(bR), (new Counter(bR.ReadInt32()), new Counter())); if (version < 6) _totalClients = count; } if (version >= 4) { int count = bR.ReadInt32(); _queries = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queries.TryAdd(new DnsQuestionRecord(bR.BaseStream), new Counter(bR.ReadInt32())); } else { _queries = new ConcurrentDictionary(1, 0); } if (version >= 5) { int count = bR.ReadInt32(); ConcurrentDictionary errorIpAddresses = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) errorIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt32())); } break; case 7: case 8: case 9: _totalQueries = bR.ReadInt64(); _totalNoError = bR.ReadInt64(); _totalServerFailure = bR.ReadInt64(); _totalNxDomain = bR.ReadInt64(); _totalRefused = bR.ReadInt64(); _totalAuthoritative = bR.ReadInt64(); _totalRecursive = bR.ReadInt64(); _totalCached = bR.ReadInt64(); _totalBlocked = bR.ReadInt64(); if (version >= 8) _totalDropped = bR.ReadInt64(); _totalClients = bR.ReadInt64(); { int count = bR.ReadInt32(); _queryDomains = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queryDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt64())); } { int count = bR.ReadInt32(); _queryBlockedDomains = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queryBlockedDomains.TryAdd(bR.ReadShortString(), new Counter(bR.ReadInt64())); } { int count = bR.ReadInt32(); _queryTypes = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queryTypes.TryAdd((DnsResourceRecordType)bR.ReadUInt16(), new Counter(bR.ReadInt64())); } if (version >= 8) { int count = bR.ReadInt32(); _protocolTypes = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _protocolTypes.TryAdd((DnsTransportProtocol)bR.ReadByte(), new Counter(bR.ReadInt64())); } else { _protocolTypes = new ConcurrentDictionary(1, 0); } if (version >= 9) { int count = bR.ReadInt32(); _clientIpAddressesUdpTcp = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _clientIpAddressesUdpTcp.TryAdd(IPAddressExtensions.ReadFrom(bR), (new Counter(bR.ReadInt64()), new Counter(bR.ReadInt64()))); } else { int count = bR.ReadInt32(); _clientIpAddressesUdpTcp = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _clientIpAddressesUdpTcp.TryAdd(IPAddressExtensions.ReadFrom(bR), (new Counter(bR.ReadInt64()), new Counter())); } { int count = bR.ReadInt32(); _queries = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) _queries.TryAdd(new DnsQuestionRecord(bR.BaseStream), new Counter(bR.ReadInt64())); } if (version <= 8) { int count = bR.ReadInt32(); ConcurrentDictionary errorIpAddresses = new ConcurrentDictionary(1, count); for (int i = 0; i < count; i++) errorIpAddresses.TryAdd(IPAddressExtensions.ReadFrom(bR), new Counter(bR.ReadInt64())); } break; default: throw new InvalidDataException("StatCounter version not supported."); } _locked = true; } #endregion #region private private static List> GetTopList(List> list, int limit) where T : DashboardStats.TopStats { list.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.Hits.CompareTo(item1.Value.Hits); }); if (list.Count > limit) list.RemoveRange(limit, list.Count - limit); return list; } private static Counter GetNewCounter(T key) { return new Counter(); } private static (Counter, Counter) GetNewCounterTuple(T key) { return (new Counter(), new Counter()); } #endregion #region public public void Lock() { _locked = true; } public void Update(DnsQuestionRecord query, DnsResponseCode responseCode, DnsServerResponseType responseType, IPAddress clientIpAddress, DnsTransportProtocol protocol, bool rateLimited) { if (_locked) return; if (clientIpAddress.IsIPv4MappedToIPv6) clientIpAddress = clientIpAddress.MapToIPv4(); _totalQueries++; if (responseType == DnsServerResponseType.Dropped) { _totalDropped++; if (rateLimited) { if (protocol == DnsTransportProtocol.Udp) _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item1.Increment(); else _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item2.Increment(); _totalClients = _clientIpAddressesUdpTcp.Count; } } else { switch (responseCode) { case DnsResponseCode.NoError: if (query is not null) { switch (responseType) { case DnsServerResponseType.Blocked: case DnsServerResponseType.UpstreamBlocked: case DnsServerResponseType.UpstreamBlockedCached: //skip blocked domains break; default: _queryDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment(); _queries.GetOrAdd(query, GetNewCounter).Increment(); break; } } _totalNoError++; break; case DnsResponseCode.ServerFailure: _totalServerFailure++; break; case DnsResponseCode.NxDomain: _totalNxDomain++; break; case DnsResponseCode.Refused: _totalRefused++; break; case DnsResponseCode.FormatError: break; } switch (responseType) { case DnsServerResponseType.Authoritative: _totalAuthoritative++; break; case DnsServerResponseType.Recursive: _totalRecursive++; break; case DnsServerResponseType.Cached: _totalCached++; break; case DnsServerResponseType.Blocked: if (query is not null) _queryBlockedDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment(); _totalBlocked++; break; case DnsServerResponseType.UpstreamBlocked: _totalRecursive++; if (query is not null) _queryBlockedDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment(); _totalBlocked++; break; case DnsServerResponseType.UpstreamBlockedCached: _totalCached++; if (query is not null) _queryBlockedDomains.GetOrAdd(query.Name.ToLowerInvariant(), GetNewCounter).Increment(); _totalBlocked++; break; } if (query is not null) _queryTypes.GetOrAdd(query.Type, GetNewCounter).Increment(); if (protocol == DnsTransportProtocol.Udp) _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item1.Increment(); else _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress, GetNewCounterTuple).Item2.Increment(); _totalClients = _clientIpAddressesUdpTcp.Count; } _protocolTypes.GetOrAdd(protocol, GetNewCounter).Increment(); } public void Merge(StatCounter statCounter, bool isDailyStatCounter = false, bool skipLock = false) { if (!skipLock && (!_locked || !statCounter._locked)) throw new DnsServerException("StatCounter must be locked."); _totalQueries += statCounter._totalQueries; _totalNoError += statCounter._totalNoError; _totalServerFailure += statCounter._totalServerFailure; _totalNxDomain += statCounter._totalNxDomain; _totalRefused += statCounter._totalRefused; _totalAuthoritative += statCounter._totalAuthoritative; _totalRecursive += statCounter._totalRecursive; _totalCached += statCounter._totalCached; _totalBlocked += statCounter._totalBlocked; _totalDropped += statCounter._totalDropped; foreach (KeyValuePair queryDomain in statCounter._queryDomains) _queryDomains.GetOrAdd(queryDomain.Key, GetNewCounter).Merge(queryDomain.Value); foreach (KeyValuePair queryBlockedDomain in statCounter._queryBlockedDomains) _queryBlockedDomains.GetOrAdd(queryBlockedDomain.Key, GetNewCounter).Merge(queryBlockedDomain.Value); foreach (KeyValuePair queryType in statCounter._queryTypes) _queryTypes.GetOrAdd(queryType.Key, GetNewCounter).Merge(queryType.Value); foreach (KeyValuePair protocolType in statCounter._protocolTypes) _protocolTypes.GetOrAdd(protocolType.Key, GetNewCounter).Merge(protocolType.Value); foreach (KeyValuePair clientIpAddress in statCounter._clientIpAddressesUdpTcp) { (Counter, Counter) counterTuple = _clientIpAddressesUdpTcp.GetOrAdd(clientIpAddress.Key, GetNewCounterTuple); counterTuple.Item1.Merge(clientIpAddress.Value.Item1); counterTuple.Item2.Merge(clientIpAddress.Value.Item2); } foreach (KeyValuePair query in statCounter._queries) _queries.GetOrAdd(query.Key, GetNewCounter).Merge(query.Value); _totalClients = _clientIpAddressesUdpTcp.Count; _totalClientsDailyStatsSummation += statCounter._totalClients; if (isDailyStatCounter && (statCounter._totalClients > statCounter._clientIpAddressesUdpTcp.Count)) _truncationFoundDuringMerge = true; } public bool Truncate(int limit) { bool truncated = false; if (_queryDomains.Count > limit) { List> topDomains = new List>(_queryDomains); _queryDomains.Clear(); topDomains.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.Count.CompareTo(item1.Value.Count); }); if (topDomains.Count > limit) topDomains.RemoveRange(limit, topDomains.Count - limit); foreach (KeyValuePair item in topDomains) _queryDomains[item.Key] = item.Value; truncated = true; } if (_queryBlockedDomains.Count > limit) { List> topBlockedDomains = new List>(_queryBlockedDomains); _queryBlockedDomains.Clear(); topBlockedDomains.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.Count.CompareTo(item1.Value.Count); }); if (topBlockedDomains.Count > limit) topBlockedDomains.RemoveRange(limit, topBlockedDomains.Count - limit); foreach (KeyValuePair item in topBlockedDomains) _queryBlockedDomains[item.Key] = item.Value; truncated = true; } if (_queryTypes.Count > limit) { List> queryTypes = new List>(_queryTypes); _queryTypes.Clear(); queryTypes.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.Count.CompareTo(item1.Value.Count); }); if (queryTypes.Count > limit) { long othersCount = 0; for (int i = limit; i < queryTypes.Count; i++) othersCount += queryTypes[i].Value.Count; queryTypes.RemoveRange(limit - 1, queryTypes.Count - (limit - 1)); queryTypes.Add(new KeyValuePair(DnsResourceRecordType.Unknown, new Counter(othersCount))); } foreach (KeyValuePair item in queryTypes) _queryTypes[item.Key] = item.Value; truncated = true; } if (_clientIpAddressesUdpTcp.Count > limit) { List> topClients = new List>(_clientIpAddressesUdpTcp); _clientIpAddressesUdpTcp.Clear(); topClients.Sort(delegate (KeyValuePair x, KeyValuePair y) { long x1 = x.Value.Item1.Count + x.Value.Item2.Count; long y1 = y.Value.Item1.Count + y.Value.Item2.Count; return y1.CompareTo(x1); }); if (topClients.Count > limit) topClients.RemoveRange(limit, topClients.Count - limit); foreach (KeyValuePair item in topClients) _clientIpAddressesUdpTcp[item.Key] = item.Value; truncated = true; } if (_queries.Count > limit) { //only last hour queries data is required for cache auto prefetching _queries.Clear(); truncated = true; } return truncated; } public void WriteTo(BinaryWriter bW) { if (!_locked) throw new DnsServerException("StatCounter must be locked."); bW.Write(Encoding.ASCII.GetBytes("SC")); //format bW.Write((byte)9); //version bW.Write(_totalQueries); bW.Write(_totalNoError); bW.Write(_totalServerFailure); bW.Write(_totalNxDomain); bW.Write(_totalRefused); bW.Write(_totalAuthoritative); bW.Write(_totalRecursive); bW.Write(_totalCached); bW.Write(_totalBlocked); bW.Write(_totalDropped); bW.Write(_totalClients); { bW.Write(_queryDomains.Count); foreach (KeyValuePair queryDomain in _queryDomains) { bW.WriteShortString(queryDomain.Key); bW.Write(queryDomain.Value.Count); } } { bW.Write(_queryBlockedDomains.Count); foreach (KeyValuePair queryBlockedDomain in _queryBlockedDomains) { bW.WriteShortString(queryBlockedDomain.Key); bW.Write(queryBlockedDomain.Value.Count); } } { bW.Write(_queryTypes.Count); foreach (KeyValuePair queryType in _queryTypes) { bW.Write((ushort)queryType.Key); bW.Write(queryType.Value.Count); } } { bW.Write(_protocolTypes.Count); foreach (KeyValuePair protocolType in _protocolTypes) { bW.Write((byte)protocolType.Key); bW.Write(protocolType.Value.Count); } } { bW.Write(_clientIpAddressesUdpTcp.Count); foreach (KeyValuePair clientIpAddress in _clientIpAddressesUdpTcp) { clientIpAddress.Key.WriteTo(bW); bW.Write(clientIpAddress.Value.Item1.Count); bW.Write(clientIpAddress.Value.Item2.Count); } } { bW.Write(_queries.Count); foreach (KeyValuePair query in _queries) { query.Key.WriteTo(bW.BaseStream, null); bW.Write(query.Value.Count); } } } public DashboardStats.StatsData GetStatsData() { return new DashboardStats.StatsData { TotalQueries = _totalQueries, TotalNoError = _totalNoError, TotalServerFailure = _totalServerFailure, TotalNxDomain = _totalNxDomain, TotalRefused = _totalRefused, TotalAuthoritative = _totalAuthoritative, TotalRecursive = _totalRecursive, TotalCached = _totalCached, TotalBlocked = _totalBlocked, TotalDropped = _totalDropped, TotalClients = _totalClients }; } public DashboardStats.ChartData GetQueryResponseChartData() { return new DashboardStats.ChartData() { Labels = [ "Authoritative", "Recursive", "Cached", "Blocked", "Dropped" ], DataSets = [ new DashboardStats.DataSet() { Data = [ _totalAuthoritative, _totalRecursive, _totalCached, _totalBlocked, _totalDropped ] } ] }; } public DashboardStats.TopStats[] GetTopDomainStats(int limit) { List> topDomainsList = new List>(_queryDomains.Count); foreach (KeyValuePair item in _queryDomains) topDomainsList.Add(new KeyValuePair(item.Key, new DashboardStats.TopStats { Name = item.Key, Hits = item.Value.Count })); List> topDomainsData = GetTopList(topDomainsList, limit); DashboardStats.TopStats[] topDomains = new DashboardStats.TopStats[topDomainsData.Count]; for (int i = 0; i < topDomainsData.Count; i++) topDomains[i] = topDomainsData[i].Value; return topDomains; } public DashboardStats.TopStats[] GetTopBlockedDomainStats(int limit) { List> topBlockedDomainsList = new List>(_queryBlockedDomains.Count); foreach (KeyValuePair item in _queryBlockedDomains) topBlockedDomainsList.Add(new KeyValuePair(item.Key, new DashboardStats.TopStats { Name = item.Key, Hits = item.Value.Count })); List> topBlockedDomainsData = GetTopList(topBlockedDomainsList, limit); DashboardStats.TopStats[] topBlockedDomains = new DashboardStats.TopStats[topBlockedDomainsData.Count]; for (int i = 0; i < topBlockedDomainsData.Count; i++) topBlockedDomains[i] = topBlockedDomainsData[i].Value; return topBlockedDomains; } public DashboardStats.TopClientStats[] GetTopClientStats(int limit) { List> topClientsList = new List>(_clientIpAddressesUdpTcp.Count); foreach (KeyValuePair item in _clientIpAddressesUdpTcp) topClientsList.Add(new KeyValuePair(item.Key.ToString(), new DashboardStats.TopClientStats { Name = item.Key.ToString(), Hits = item.Value.Item1.Count + item.Value.Item2.Count })); List> topClientsData = GetTopList(topClientsList, limit); DashboardStats.TopClientStats[] topClients = new DashboardStats.TopClientStats[topClientsData.Count]; for (int i = 0; i < topClientsData.Count; i++) topClients[i] = topClientsData[i].Value; return topClients; } public DashboardStats.ChartData GetTopQueryTypesChartData() { List> queryTypes = new List>(_queryTypes.Count); foreach (KeyValuePair item in _queryTypes) queryTypes.Add(new KeyValuePair(item.Key.ToString(), item.Value.Count)); queryTypes.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.CompareTo(item1.Value); }); string[] queryTypeLabels = new string[queryTypes.Count]; long[] queryTypeData = new long[queryTypes.Count]; for (int i = 0; i < queryTypes.Count; i++) { KeyValuePair topQueryTypeData = queryTypes[i]; queryTypeLabels[i] = topQueryTypeData.Key; queryTypeData[i] = topQueryTypeData.Value; } return new DashboardStats.ChartData() { Labels = queryTypeLabels, DataSets = [ new DashboardStats.DataSet() { Data = queryTypeData } ] }; } public DashboardStats.ChartData GetTopProtocolTypesChartData() { List> protocolTypes = new List>(_protocolTypes.Count); foreach (KeyValuePair protocolType in _protocolTypes) protocolTypes.Add(new KeyValuePair(protocolType.Key.ToString(), protocolType.Value.Count)); protocolTypes.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.CompareTo(item1.Value); }); string[] topProtocolLabels = new string[protocolTypes.Count]; long[] topProtocolData = new long[protocolTypes.Count]; for (int i = 0; i < protocolTypes.Count; i++) { KeyValuePair topProtocolTypeData = protocolTypes[i]; topProtocolLabels[i] = topProtocolTypeData.Key; topProtocolData[i] = topProtocolTypeData.Value; } return new DashboardStats.ChartData() { Labels = topProtocolLabels, DataSets = [ new DashboardStats.DataSet() { Data = topProtocolData } ] }; } public List> GetEligibleQueries(int minimumHits) { List> eligibleQueries = new List>(Convert.ToInt32(_queries.Count * 0.1)); foreach (KeyValuePair item in _queries) { if (item.Value.Count >= minimumHits) eligibleQueries.Add(new KeyValuePair(item.Key, item.Value.Count)); } return eligibleQueries; } public Dictionary GetClientSubnetStats(IEnumerable ipv4Prefixes, IEnumerable ipv6Prefixes) { Dictionary clientSubnetStats = new Dictionary(_clientIpAddressesUdpTcp.Count); void UpdateClientSubnetStats(NetworkAddress clientSubnet, (Counter, Counter) value) { if (clientSubnetStats.TryGetValue(clientSubnet, out ValueTuple existingValue)) { existingValue.Item1 += value.Item1.Count; existingValue.Item2 += value.Item2.Count; } else { clientSubnetStats.Add(clientSubnet, (value.Item1.Count, value.Item2.Count)); } } foreach (KeyValuePair item in _clientIpAddressesUdpTcp) { switch (item.Key.AddressFamily) { case AddressFamily.InterNetwork: IPAddress clientIPv4 = item.Key; foreach (int ipv4Prefix in ipv4Prefixes) UpdateClientSubnetStats(new NetworkAddress(clientIPv4, (byte)ipv4Prefix), item.Value); break; case AddressFamily.InterNetworkV6: IPAddress clientIPv6 = item.Key; foreach (int ipv6Prefix in ipv6Prefixes) UpdateClientSubnetStats(new NetworkAddress(clientIPv6, (byte)ipv6Prefix), item.Value); break; default: throw new NotSupportedException("AddressFamily not supported."); } } return clientSubnetStats; } #endregion #region properties public bool IsLocked { get { return _locked; } } public long TotalQueries { get { return _totalQueries; } } public long TotalNoError { get { return _totalNoError; } } public long TotalServerFailure { get { return _totalServerFailure; } } public long TotalNxDomain { get { return _totalNxDomain; } } public long TotalRefused { get { return _totalRefused; } } public long TotalAuthoritative { get { return _totalAuthoritative; } } public long TotalRecursive { get { return _totalRecursive; } } public long TotalCached { get { return _totalCached; } } public long TotalBlocked { get { return _totalBlocked; } } public long TotalDropped { get { return _totalDropped; } } public long TotalClients { get { if (_truncationFoundDuringMerge) return _totalClientsDailyStatsSummation; return _totalClients; } } #endregion class Counter { #region variables long _count; #endregion #region constructor public Counter() { } public Counter(long count) { _count = count; } #endregion #region public public void Increment() { _count++; } public void Merge(Counter counter) { _count += counter._count; } #endregion #region properties public long Count { get { return _count; } } #endregion } } readonly struct StatsQueueItem { #region variables public readonly DateTime _timestamp; public readonly DnsDatagram _request; public readonly IPEndPoint _remoteEP; public readonly DnsTransportProtocol _protocol; public readonly DnsDatagram _response; public readonly bool _rateLimited; #endregion #region constructor public StatsQueueItem(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response, bool rateLimited) { _timestamp = DateTime.UtcNow; _request = request; _remoteEP = remoteEP; _protocol = protocol; _response = response; _rateLimited = rateLimited; } #endregion } } } ================================================ FILE: DnsServerCore/Dns/Trees/AuthZoneNode.cs ================================================ /* Technitium DNS Server Copyright (C) 2022 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.Dns.Zones; using System; using System.Collections.Generic; using System.Threading; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Trees { class AuthZoneNode : IDisposable { #region variables SubDomainZone _parentSideZone; ApexZone _apexZone; #endregion #region constructors public AuthZoneNode(SubDomainZone parentSideZone, ApexZone zone) { _parentSideZone = parentSideZone; _apexZone = zone; } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; if (_apexZone is not null) _apexZone.Dispose(); _disposed = true; } #endregion #region public public bool TryAdd(ApexZone apexZone) { return Interlocked.CompareExchange(ref _apexZone, apexZone, null) is null; } public bool TryAdd(SubDomainZone parentSideZone) { return Interlocked.CompareExchange(ref _parentSideZone, parentSideZone, null) is null; } public bool TryRemove(out ApexZone apexZone) { apexZone = _apexZone; return ReferenceEquals(Interlocked.CompareExchange(ref _apexZone, null, apexZone), apexZone); } public bool TryRemove(out SubDomainZone parentSideZone) { parentSideZone = _parentSideZone; return ReferenceEquals(Interlocked.CompareExchange(ref _parentSideZone, null, parentSideZone), parentSideZone); } public SubDomainZone GetOrAddParentSideZone(Func valueFactory) { SubDomainZone newParentSideZone = null; while (true) { SubDomainZone parentSideZone = _parentSideZone; if (parentSideZone is not null) return parentSideZone; if (newParentSideZone is null) newParentSideZone = valueFactory(); if (TryAdd(newParentSideZone)) return newParentSideZone; } } public IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { if ((_apexZone is null) || (type == DnsResourceRecordType.DS)) { if (_parentSideZone is null) return Array.Empty(); return _parentSideZone.QueryRecords(type, dnssecOk); } return _apexZone.QueryRecords(type, dnssecOk); } public AuthZone GetAuthZone(string zoneName) { if ((_apexZone is not null) && _apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) return _apexZone; return _parentSideZone; } #endregion #region properties public string Name { get { if (_parentSideZone is not null) return _parentSideZone.Name; if (_apexZone is not null) return _apexZone.Name; return null; } } public SubDomainZone ParentSideZone { get { return _parentSideZone; } } public ApexZone ApexZone { get { return _apexZone; } } public bool IsActive { get { if (_apexZone is not null) return _apexZone.IsActive; if (_parentSideZone is not null) return _parentSideZone.IsActive; return false; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Trees/AuthZoneTree.cs ================================================ /* 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.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.Threading; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Trees { class AuthZoneTree : ZoneTree { #region variables static readonly char[] _starPeriodTrimChars = new char[] { '*', '.' }; #endregion #region private private static Node GetPreviousSubDomainZoneNode(byte[] key, Node currentNode, int baseDepth) { int k; NodeValue currentValue = currentNode.Value; if (currentValue is null) { //key value does not exists if (currentNode.Children is null) { //no children available; move to previous sibling k = currentNode.K - 1; //find previous node from sibling starting at k - 1 currentNode = currentNode.Parent; } else { if (key.Length == currentNode.Depth) { //current node belongs to the key k = currentNode.K - 1; //find previous node from sibling starting at k - 1 currentNode = currentNode.Parent; } else { //find the previous node for the given k in current node's children k = key[currentNode.Depth]; } } } else { int x = DnsNSECRecordData.CanonicalComparison(currentValue.Key, key); if (x == 0) { //current node value matches the key k = currentNode.K - 1; //find previous node from sibling starting at k - 1 currentNode = currentNode.Parent; } else if (x > 0) { //current node value is larger for the key k = currentNode.K - 1; //find previous node from sibling starting at k - 1 currentNode = currentNode.Parent; } else { //current node value is smaller for the key if (currentNode.Children is null) { //the current node is previous node since no children exists and value is smaller for the key return currentNode; } else { //find the previous node for the given k in current node's children k = key[currentNode.Depth]; } } } //start reverse tree traversal while ((currentNode is not null) && (currentNode.Depth >= baseDepth)) { Node[] children = currentNode.Children; if (children is not null) { //find previous child node Node child = null; for (int i = k; i > -1; i--) { child = Volatile.Read(ref children[i]); if (child is not null) { bool childNodeHasApexZone = false; NodeValue childValue = child.Value; if (childValue is not null) { AuthZoneNode authZoneNode = childValue.Value; if (authZoneNode is not null) { if (authZoneNode.ApexZone is not null) childNodeHasApexZone = true; //must stop checking children of the apex of the sub zone } } if (!childNodeHasApexZone && child.Children is not null) break; //child has further children so check them first if (childValue is not null) { AuthZoneNode authZoneNode = childValue.Value; if (authZoneNode is not null) { if (authZoneNode.ParentSideZone is not null) { //is sub domain zone return child; //child has value so return it } if (authZoneNode.ApexZone is not null) { //is apex zone //skip to next child to avoid listing this auth zone's sub domains child = null; //set null to avoid child being set as current after the loop } } } } } if (child is not null) { //make found child as current k = children.Length - 1; currentNode = child; continue; //start over } } //no child node available; check for current node value { NodeValue value = currentNode.Value; if (value is not null) { AuthZoneNode authZoneNode = value.Value; if (authZoneNode is not null) { if ((authZoneNode.ApexZone is not null) && (currentNode.Depth == baseDepth)) { //current node contains apex zone for the base depth i.e. current zone; return it return currentNode; } if (authZoneNode.ParentSideZone is not null) { //current node contains sub domain zone; return it return currentNode; } } } } //move up to parent node for previous sibling k = currentNode.K - 1; currentNode = currentNode.Parent; } return null; } private static Node GetNextSubDomainZoneNode(byte[] key, Node currentNode, int baseDepth) { int k; NodeValue currentValue = currentNode.Value; if (currentValue is null) { //key value does not exists if (currentNode.Children is null) { //no children available; move to next sibling k = currentNode.K + 1; //find next node from sibling starting at k + 1 currentNode = currentNode.Parent; } else { if (key.Length == currentNode.Depth) { //current node belongs to the key k = 0; //find next node from first child of current node } else { //find next node for the given k in current node's children k = key[currentNode.Depth]; } } } else { //check if node contains apex zone bool foundApexZone = false; if (currentNode.Depth > baseDepth) { AuthZoneNode authZoneNode = currentValue.Value; if (authZoneNode is not null) { ApexZone apexZone = authZoneNode.ApexZone; if (apexZone is not null) foundApexZone = true; } } if (foundApexZone) { //current contains apex for a sub zone; move up to parent node k = currentNode.K + 1; //find next node from sibling starting at k + 1 currentNode = currentNode.Parent; } else { int x = DnsNSECRecordData.CanonicalComparison(currentValue.Key, key); if (x == 0) { //current node value matches the key k = 0; //find next node from children starting at k } else if (x > 0) { //current node value is larger for the key thus current is the next node return currentNode; } else { //current node value is smaller for the key k = key[currentNode.Depth]; //find next node from children starting at k = key[depth] } } } //start tree traversal while ((currentNode is not null) && (currentNode.Depth >= baseDepth)) { Node[] children = currentNode.Children; if (children is not null) { //find next child node Node child = null; for (int i = k; i < children.Length; i++) { child = Volatile.Read(ref children[i]); if (child is not null) { NodeValue childValue = child.Value; if (childValue is not null) { AuthZoneNode authZoneNode = childValue.Value; if (authZoneNode is not null) { if (authZoneNode.ParentSideZone is not null) { //is sub domain zone return child; //child has value so return it } if (authZoneNode.ApexZone is not null) { //is apex zone //skip to next child to avoid listing this auth zone's sub domains child = null; //set null to avoid child being set as current after the loop continue; } } } if (child.Children is not null) break; } } if (child is not null) { //make found child as current k = 0; currentNode = child; continue; //start over } } //no child nodes available; move up to parent node k = currentNode.K + 1; currentNode = currentNode.Parent; } return null; } private static bool SubDomainExists(byte[] key, Node currentNode) { Node[] children = currentNode.Children; if (children is not null) { Node child = Volatile.Read(ref children[1]); //[*] if (child is not null) return true; //wildcard exists so subdomain name exists: RFC 4592 section 4.9 } Node nextSubDomain = GetNextSubDomainZoneNode(key, currentNode, currentNode.Depth); if (nextSubDomain is null) return false; NodeValue value = nextSubDomain.Value; if (value is null) return false; return IsKeySubDomain(key, value.Key, false); } private static AuthZone GetAuthZoneFromNode(Node node, string zoneName) { NodeValue value = node.Value; if (value is not null) { AuthZoneNode authZoneNode = value.Value; if (authZoneNode is not null) return authZoneNode.GetAuthZone(zoneName); } return null; } private void RemoveAllSubDomains(string domain, Node currentNode) { //remove all sub domains under current zone Node current = currentNode; byte[] currentKey = ConvertToByteKey(domain); do { current = GetNextSubDomainZoneNode(currentKey, current, currentNode.Depth); if (current is null) break; NodeValue v = current.Value; if (v is not null) { AuthZoneNode z = v.Value; if (z is not null) { if (z.ApexZone is null) { //no apex zone at this node; remove complete zone node current.RemoveNodeValue(v.Key, out _); //remove node value current.CleanThisBranch(); } else { //apex node exists; remove parent size sub domain z.TryRemove(out SubDomainZone _); } } currentKey = v.Key; } } while (true); } #endregion #region protected protected override void GetClosestValuesForZone(AuthZoneNode zoneValue, out SubDomainZone closestSubDomain, out SubDomainZone closestDelegation, out ApexZone closestAuthority) { ApexZone apexZone = zoneValue.ApexZone; if (apexZone is not null) { //hosted primary/secondary/stub/forwarder zone found closestSubDomain = null; closestDelegation = zoneValue.ParentSideZone; closestAuthority = apexZone; } else { //hosted sub domain SubDomainZone subDomainZone = zoneValue.ParentSideZone; if (subDomainZone.ContainsNameServerRecords()) { //delegated sub domain found closestSubDomain = null; closestDelegation = subDomainZone; } else { closestSubDomain = subDomainZone; closestDelegation = null; } closestAuthority = null; } } #endregion #region public public bool TryAdd(ApexZone zone) { AuthZoneNode zoneNode = GetOrAdd(zone.Name, delegate (string key) { return new AuthZoneNode(null, zone); }); if (ReferenceEquals(zoneNode.ApexZone, zone)) return true; //added successfully return zoneNode.TryAdd(zone); } public bool TryGet(string zoneName, string domain, out AuthZone authZone) { if (TryGet(domain, out AuthZoneNode authZoneNode)) { authZone = authZoneNode.GetAuthZone(zoneName); return authZone is not null; } authZone = null; return false; } public bool TryGet(string zoneName, out ApexZone apexZone) { if (TryGet(zoneName, out AuthZoneNode authZoneNode) && (authZoneNode.ApexZone is not null)) { apexZone = authZoneNode.ApexZone; return true; } apexZone = null; return false; } public bool TryRemove(string domain, out ApexZone apexZone) { if (!TryGet(domain, out AuthZoneNode authZoneNode, out Node currentNode) || (authZoneNode.ApexZone is null)) { apexZone = null; return false; } apexZone = authZoneNode.ApexZone; if (authZoneNode.ParentSideZone is null) { //remove complete zone node if (!base.TryRemove(domain, out AuthZoneNode _)) { apexZone = null; return false; } } else { //parent side sub domain exists; remove only apex zone from zone node if (!authZoneNode.TryRemove(out ApexZone _)) { apexZone = null; return false; } } //remove all sub domains under current apex zone RemoveAllSubDomains(domain, currentNode); currentNode.CleanThisBranch(); return true; } public bool TryRemove(string domain, out SubDomainZone subDomainZone, bool removeAllSubDomains = false) { if (!TryGet(domain, out AuthZoneNode zoneNode, out Node currentNode) || (zoneNode.ParentSideZone is null)) { subDomainZone = null; return false; } subDomainZone = zoneNode.ParentSideZone; if (zoneNode.ApexZone is null) { //remove complete zone node if (!base.TryRemove(domain, out AuthZoneNode _)) { subDomainZone = null; return false; } } else { //apex zone exists; remove only parent side sub domain from zone node if (!zoneNode.TryRemove(out SubDomainZone _)) { subDomainZone = null; return false; } } if (removeAllSubDomains) RemoveAllSubDomains(domain, currentNode); //remove all sub domains under current subdomain zone currentNode.CleanThisBranch(); return true; } public override bool TryRemove(string key, out AuthZoneNode authZoneNode) { throw new InvalidOperationException(); } public IReadOnlyList GetApexZoneWithSubDomainZones(string zoneName) { List zones = new List(); byte[] key = ConvertToByteKey(zoneName); NodeValue nodeValue = _root.FindNodeValue(key, out Node currentNode); if (nodeValue is not null) { AuthZoneNode authZoneNode = nodeValue.Value; if (authZoneNode is not null) { ApexZone apexZone = authZoneNode.ApexZone; if (apexZone is not null) { zones.Add(apexZone); Node current = currentNode; byte[] currentKey = key; do { current = GetNextSubDomainZoneNode(currentKey, current, currentNode.Depth); if (current is null) break; NodeValue value = current.Value; if (value is not null) { authZoneNode = value.Value; if (authZoneNode is not null) zones.Add(authZoneNode.ParentSideZone); currentKey = value.Key; } } while (true); } } } return zones; } public IReadOnlyList GetSubDomainZoneWithSubDomainZones(string domain) { List zones = new List(); byte[] key = ConvertToByteKey(domain); NodeValue nodeValue = _root.FindNodeValue(key, out Node currentNode); if (nodeValue is not null) { AuthZoneNode authZoneNode = nodeValue.Value; if (authZoneNode is not null) { SubDomainZone subDomainZone = authZoneNode.ParentSideZone; if (subDomainZone is not null) { zones.Add(subDomainZone); Node current = currentNode; byte[] currentKey = key; do { current = GetNextSubDomainZoneNode(currentKey, current, currentNode.Depth); if (current is null) break; NodeValue value = current.Value; if (value is not null) { authZoneNode = value.Value; if (authZoneNode is not null) zones.Add(authZoneNode.ParentSideZone); currentKey = value.Key; } } while (true); } } } return zones; } public AuthZone GetOrAddSubDomainZone(string zoneName, string domain, Func valueFactory) { bool isApex = zoneName.Equals(domain, StringComparison.OrdinalIgnoreCase); AuthZoneNode authZoneNode = GetOrAdd(domain, delegate (string key) { if (isApex) throw new DnsServerException("Zone was not found for domain: " + key); return new AuthZoneNode(valueFactory(), null); }); if (isApex) { if (authZoneNode.ApexZone is null) throw new DnsServerException("Zone was not found: " + zoneName); return authZoneNode.ApexZone; } else { return authZoneNode.GetOrAddParentSideZone(valueFactory); } } public AuthZone GetAuthZone(string zoneName, string domain) { if (TryGet(domain, out AuthZoneNode authZoneNode)) return authZoneNode.GetAuthZone(zoneName); return null; } public ApexZone GetApexZone(string zoneName) { if (TryGet(zoneName, out AuthZoneNode authZoneNode)) return authZoneNode.ApexZone; return null; } public AuthZone FindZone(string domain, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone authority, out bool hasSubDomains) { byte[] key = ConvertToByteKey(domain); AuthZoneNode authZoneNode = FindZoneNode(key, true, out Node currentNode, out Node closestSubDomainNode, out _, out SubDomainZone closestSubDomain, out SubDomainZone closestDelegation, out ApexZone closestAuthority); if (authZoneNode is null) { //zone not found closest = closestSubDomain; delegation = closestDelegation; authority = closestAuthority; if (authority is null) { //no authority so no sub domains hasSubDomains = false; } else if ((closestSubDomainNode is not null) && !closestSubDomainNode.HasChildren) { //closest sub domain node does not have any children so no sub domains hasSubDomains = false; } else { //check if current node has sub domains hasSubDomains = SubDomainExists(key, currentNode); } return null; } else { //zone found AuthZone zone; ApexZone apexZone = authZoneNode.ApexZone; if (apexZone is not null) { zone = apexZone; closest = null; delegation = authZoneNode.ParentSideZone; authority = apexZone; } else { SubDomainZone subDomainZone = authZoneNode.ParentSideZone; zone = subDomainZone; if (zone == closestSubDomain) closest = null; else closest = closestSubDomain; if (closestDelegation is not null) delegation = closestDelegation; else if (subDomainZone.ContainsNameServerRecords()) delegation = subDomainZone; else delegation = null; authority = closestAuthority; } if (zone.Disabled) { if ((closestSubDomainNode is not null) && !closestSubDomainNode.HasChildren) { //closest sub domain node does not have any children so no sub domains hasSubDomains = false; } else { //check if current node has sub domains hasSubDomains = SubDomainExists(key, currentNode); } } else { //since zone is found, it does not matter if subdomain exists or not hasSubDomains = false; } return zone; } } public AuthZone FindPreviousSubDomainZone(string zoneName, string domain) { byte[] key = ConvertToByteKey(domain); AuthZoneNode authZoneNode = FindZoneNode(key, false, out Node currentNode, out _, out Node closestAuthorityNode, out _, out _, out _); if (authZoneNode is not null) { //zone exists ApexZone apexZone = authZoneNode.ApexZone; SubDomainZone parentSideZone = authZoneNode.ParentSideZone; if ((apexZone is not null) && (parentSideZone is not null)) { //found ambiguity between apex zone and sub domain zone if (!apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { //zone name does not match with apex zone and thus not match with closest authority node //find the closest authority zone for given zone name if (!TryGet(zoneName, out _, out Node closestNodeForZoneName)) throw new InvalidOperationException(); closestAuthorityNode = closestNodeForZoneName; } } } Node previousNode = GetPreviousSubDomainZoneNode(key, currentNode, closestAuthorityNode.Depth); if (previousNode is not null) { AuthZone authZone = GetAuthZoneFromNode(previousNode, zoneName); if (authZone is not null) return authZone; } return null; } public AuthZone FindNextSubDomainZone(string zoneName, string domain) { byte[] key = ConvertToByteKey(domain); AuthZoneNode authZoneNode = FindZoneNode(key, false, out Node currentNode, out _, out Node closestAuthorityNode, out _, out _, out _); if (authZoneNode is not null) { //zone exists ApexZone apexZone = authZoneNode.ApexZone; SubDomainZone parentSideZone = authZoneNode.ParentSideZone; if ((apexZone is not null) && (parentSideZone is not null)) { //found ambiguity between apex zone and sub domain zone if (!apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { //zone name does not match with apex zone and thus not match with closest authority node //find the closest authority zone for given zone name if (!TryGet(zoneName, out _, out Node closestNodeForZoneName)) throw new InvalidOperationException(); closestAuthorityNode = closestNodeForZoneName; } } } Node nextNode = GetNextSubDomainZoneNode(key, currentNode, closestAuthorityNode.Depth); if (nextNode is not null) { AuthZone authZone = GetAuthZoneFromNode(nextNode, zoneName); if (authZone is not null) return authZone; } return null; } public bool SubDomainExistsFor(string zoneName, string domain) { AuthZone nextAuthZone = FindNextSubDomainZone(zoneName, domain); if (nextAuthZone is null) return false; return nextAuthZone.Name.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase); } #endregion #region DNSSEC public IReadOnlyList FindNSecProofOfNonExistenceNxDomain(string domain, bool isWildcardAnswer) { List nsecRecords = new List(2 * 2); //add proof of cover for domain NSecAddProofOfCoverFor(domain, nsecRecords); if (isWildcardAnswer) return nsecRecords; //add proof of cover for wildcard if (nsecRecords.Count > 0) { //add wildcard proof to prove that a wildcard expansion was not possible DnsResourceRecord nsecRecord = nsecRecords[0]; DnsNSECRecordData nsec = nsecRecord.RDATA as DnsNSECRecordData; string wildcardName = DnsNSECRecordData.GetWildcardFor(nsecRecord, domain); if (!DnsNSECRecordData.IsDomainCovered(nsecRecord.Name, nsec.NextDomainName, wildcardName)) NSecAddProofOfCoverFor(wildcardName, nsecRecords); } return nsecRecords; } public IReadOnlyList FindNSec3ProofOfNonExistenceNxDomain(string domain, bool isWildcardAnswer) { List nsec3Records = new List(3 * 2); byte[] key = ConvertToByteKey(domain); string closestEncloser; AuthZoneNode authZoneNode = FindZoneNode(key, isWildcardAnswer, out _, out _, out _, out SubDomainZone closestSubDomain, out _, out ApexZone closestAuthority); if (authZoneNode is not null) { if (isWildcardAnswer && (closestSubDomain is not null) && closestSubDomain.Name.StartsWith('*')) { closestEncloser = closestSubDomain.Name.TrimStart(_starPeriodTrimChars); } else { //subdomain that contains only NSEC3 record does not really exists: RFC5155 section 7.2.8 if ((authZoneNode.ApexZone is not null) || ((authZoneNode.ParentSideZone is not null) && !authZoneNode.ParentSideZone.HasOnlyNSec3Records())) throw new InvalidOperationException($"Cannot prove non-existence: The domain name '{domain}' exists and probably got added just now."); //domain exists! cannot prove non-existence //continue to prove non-existence of this nsec3 owner name closestEncloser = closestAuthority.Name; } } else { if (closestSubDomain is not null) closestEncloser = closestSubDomain.Name; else if (closestAuthority is not null) closestEncloser = closestAuthority.Name; else throw new InvalidOperationException(); //cannot find closest encloser } IReadOnlyList nsec3ParamRecords = closestAuthority.GetRecords(DnsResourceRecordType.NSEC3PARAM); if (nsec3ParamRecords.Count == 0) throw new InvalidOperationException("Zone does not have NSEC3 deployed."); DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData; //find correct closest encloser string hashedNextCloserName; while (true) { string nextCloserName = DnsNSEC3RecordData.GetNextCloserName(domain, closestEncloser); hashedNextCloserName = nsec3Param.ComputeHashedOwnerNameBase32HexString(nextCloserName) + (closestAuthority.Name.Length > 0 ? "." + closestAuthority.Name : ""); AuthZone nsec3Zone = GetAuthZone(closestAuthority.Name, hashedNextCloserName); if (nsec3Zone is null) break; //next closer name does not exists //next closer name exists as an ENT closestEncloser = nextCloserName; if (domain.Equals(closestEncloser, StringComparison.OrdinalIgnoreCase)) { //domain exists as an ENT; return no data proof return FindNSec3ProofOfNonExistenceNoData(nsec3Zone); } } if (isWildcardAnswer) { //add proof of cover for the domain to prove non-existence (wildcard) NSec3AddProofOfCoverFor(hashedNextCloserName, closestAuthority.Name, nsec3Records); } else { //add closest encloser proof string hashedClosestEncloser = nsec3Param.ComputeHashedOwnerNameBase32HexString(closestEncloser) + (closestAuthority.Name.Length > 0 ? "." + closestAuthority.Name : ""); AuthZone nsec3Zone = GetAuthZone(closestAuthority.Name, hashedClosestEncloser); if (nsec3Zone is null) throw new InvalidOperationException(); IReadOnlyList closestEncloserProofRecords = nsec3Zone.QueryRecords(DnsResourceRecordType.NSEC3, true); if (closestEncloserProofRecords.Count == 0) throw new InvalidOperationException(); nsec3Records.AddRange(closestEncloserProofRecords); DnsResourceRecord closestEncloserProofRecord = closestEncloserProofRecords[0]; DnsNSEC3RecordData closestEncloserProof = closestEncloserProofRecord.RDATA as DnsNSEC3RecordData; //add proof of cover for the next closer name if (!DnsNSECRecordData.IsDomainCovered(closestEncloserProofRecord.Name, closestEncloserProof.NextHashedOwnerName + (closestAuthority.Name.Length > 0 ? "." + closestAuthority.Name : ""), hashedNextCloserName)) NSec3AddProofOfCoverFor(hashedNextCloserName, closestAuthority.Name, nsec3Records); //add proof of cover to prove that a wildcard expansion was not possible string wildcardDomain = closestEncloser.Length > 0 ? "*." + closestEncloser : "*"; string hashedWildcardDomainName = nsec3Param.ComputeHashedOwnerNameBase32HexString(wildcardDomain) + (closestAuthority.Name.Length > 0 ? "." + closestAuthority.Name : ""); if (!DnsNSECRecordData.IsDomainCovered(closestEncloserProofRecord.Name, closestEncloserProof.NextHashedOwnerName + (closestAuthority.Name.Length > 0 ? "." + closestAuthority.Name : ""), hashedWildcardDomainName)) NSec3AddProofOfCoverFor(hashedWildcardDomainName, closestAuthority.Name, nsec3Records); } return nsec3Records; } public IReadOnlyList FindNSecProofOfNonExistenceNoData(string domain, AuthZone zone) { List nsecRecords = null; if (zone.Name.StartsWith("*.") || zone.Name.Equals('*')) { //for wildcard case, we need to add proof of cover since validator wont be able to match qname to the NO DATA NSEC record nsecRecords = new List(4); NSecAddProofOfCoverFor(domain, nsecRecords); } IReadOnlyList nsecRecordsNoData = zone.QueryRecords(DnsResourceRecordType.NSEC, true); if (nsecRecordsNoData.Count == 0) throw new InvalidOperationException("Zone does not have NSEC deployed correctly."); if (nsecRecords is null) return nsecRecordsNoData; foreach (DnsResourceRecord nsecRecord in nsecRecordsNoData) { if (!nsecRecords.Contains(nsecRecord)) nsecRecords.Add(nsecRecord); } return nsecRecords; } public IReadOnlyList FindNSec3ProofOfNonExistenceNoData(string domain, AuthZone zone, ApexZone apexZone) { IReadOnlyList nsec3ParamRecords = apexZone.GetRecords(DnsResourceRecordType.NSEC3PARAM); if (nsec3ParamRecords.Count == 0) throw new InvalidOperationException("Zone does not have NSEC3 deployed."); DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData; List nsec3Records = null; if (zone.Name.StartsWith("*.") || zone.Name.Equals('*')) { //for wildcard case, we need to add the closest encloser and add proof of cover since validator wont be able to match qname hashed owner name to the NO DATA NSEC3 record string closestEncloser = AuthZoneManager.GetParentZone(zone.Name); if (closestEncloser is null) closestEncloser = ""; string closestEncloserHashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(closestEncloser) + (apexZone.Name.Length > 0 ? "." + apexZone.Name : ""); AuthZone nsec3ZoneClosestEncloser = GetAuthZone(apexZone.Name, closestEncloserHashedOwnerName); if (nsec3ZoneClosestEncloser is not null) { nsec3Records = new List(4); nsec3Records.AddRange(FindNSec3ProofOfNonExistenceNoData(nsec3ZoneClosestEncloser)); string qnameHashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(domain) + (apexZone.Name.Length > 0 ? "." + apexZone.Name : ""); NSec3AddProofOfCoverFor(qnameHashedOwnerName, apexZone.Name, nsec3Records); } } string hashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(zone.Name) + (apexZone.Name.Length > 0 ? "." + apexZone.Name : ""); AuthZone nsec3Zone = GetAuthZone(apexZone.Name, hashedOwnerName); if (nsec3Zone is null) { //this is probably since the domain in request is for an nsec3 record owner name return FindNSec3ProofOfNonExistenceNxDomain(zone.Name, false); } IReadOnlyList nsec3RecordsNoData = FindNSec3ProofOfNonExistenceNoData(nsec3Zone); if (nsec3Records is null) return nsec3RecordsNoData; foreach (DnsResourceRecord nsec3Record in nsec3RecordsNoData) { if (!nsec3Records.Contains(nsec3Record)) nsec3Records.Add(nsec3Record); } return nsec3Records; } private static IReadOnlyList FindNSec3ProofOfNonExistenceNoData(AuthZone nsec3Zone) { IReadOnlyList nsec3Records = nsec3Zone.QueryRecords(DnsResourceRecordType.NSEC3, true); if (nsec3Records.Count > 0) return nsec3Records; return Array.Empty(); } private void NSecAddProofOfCoverFor(string domain, List nsecRecords) { byte[] key = ConvertToByteKey(domain); AuthZoneNode authZoneNode = FindZoneNode(key, false, out Node currentNode, out _, out Node closestAuthorityNode, out _, out _, out ApexZone closestAuthority); if (authZoneNode is not null) throw new InvalidOperationException($"Cannot prove non-existence: The domain name '{domain}' exists and probably got added just now."); //domain exists! cannot prove non-existence Node previousNode = GetPreviousSubDomainZoneNode(key, currentNode, closestAuthorityNode.Depth); if (previousNode is not null) { AuthZone authZone = GetAuthZoneFromNode(previousNode, closestAuthority.Name); if (authZone is not null) { IReadOnlyList proofOfCoverRecords = authZone.QueryRecords(DnsResourceRecordType.NSEC, true); foreach (DnsResourceRecord proofOfCoverRecord in proofOfCoverRecords) { if (!nsecRecords.Contains(proofOfCoverRecord)) nsecRecords.Add(proofOfCoverRecord); } } } } private void NSec3AddProofOfCoverFor(string hashedOwnerName, string zoneName, List nsec3Records) { IReadOnlyList TryFindPreviousNSec3Records(string ownerName) { while (true) { AuthZone zone = FindPreviousSubDomainZone(zoneName, ownerName); if (zone is null) return null; //no previous auth zone found IReadOnlyList previousNSec3Records = zone.QueryRecords(DnsResourceRecordType.NSEC3, true); if (previousNSec3Records.Count > 0) return previousNSec3Records; //found proof of cover ownerName = zone.Name; } } //find previous NSEC3 for the hashed owner name IReadOnlyList proofOfCoverRecords = TryFindPreviousNSec3Records(hashedOwnerName); if (proofOfCoverRecords is null) { //didnt find previous NSEC3; find the last NSEC3 which will give the proof of cover proofOfCoverRecords = TryFindPreviousNSec3Records("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz0" + (zoneName.Length > 0 ? "." + zoneName : "")); } if (proofOfCoverRecords is null) throw new InvalidOperationException(); foreach (DnsResourceRecord proofOfCoverRecord in proofOfCoverRecords) { if (!nsec3Records.Contains(proofOfCoverRecord)) nsec3Records.Add(proofOfCoverRecord); } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Trees/CacheZoneTree.cs ================================================ /* Technitium DNS Server Copyright (C) 2022 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.Dns.Zones; namespace DnsServerCore.Dns.Trees { class CacheZoneTree : ZoneTree { #region protected protected override void GetClosestValuesForZone(CacheZone zoneValue, out CacheZone closestSubDomain, out CacheZone closestDelegation, out CacheZone closestAuthority) { if (zoneValue.ContainsNameServerRecords()) { //ns records found closestSubDomain = null; closestDelegation = zoneValue; } else { closestSubDomain = zoneValue; closestDelegation = null; } closestAuthority = null; } #endregion #region public public bool TryRemoveTree(string domain, out CacheZone value, out int removedEntries) { bool removed = TryRemove(domain, out value, out Node currentNode); if (removed) removedEntries = value.TotalEntries; else removedEntries = 0; //remove all cache zones under current zone Node current = currentNode; do { current = current.GetNextNodeWithValue(currentNode.Depth); if (current is null) break; NodeValue v = current.Value; if (v is not null) { CacheZone zone = v.Value; if (zone is not null) { current.RemoveNodeValue(v.Key, out _); //remove node value current.CleanThisBranch(); removed = true; removedEntries += zone.TotalEntries; } } } while (true); if (removed) currentNode.CleanThisBranch(); return removed; } public CacheZone FindZone(string domain, out CacheZone closest, out CacheZone delegation) { byte[] key = ConvertToByteKey(domain); CacheZone zoneValue = FindZoneNode(key, false, out _, out _, out _, out CacheZone closestSubDomain, out CacheZone closestDelegation, out _); if (zoneValue is null) { //zone not found closest = closestSubDomain; //required for DNAME delegation = closestDelegation; return null; } else { //zone found closest = null; //not required if (zoneValue.ContainsNameServerRecords()) delegation = zoneValue; else if (closestDelegation is not null) delegation = closestDelegation; else delegation = null; return zoneValue; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Trees/DomainTree.cs ================================================ /* 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 System; using System.Text; using TechnitiumLibrary.ByteTree; namespace DnsServerCore.Dns.Trees { class DomainTree : ByteTree where T : class { #region variables readonly static byte[] _keyMap; readonly static byte[] _reverseKeyMap; #endregion #region constructor static DomainTree() { _keyMap = new byte[256]; _reverseKeyMap = new byte[41]; int keyCode; for (int i = 0; i < _keyMap.Length; i++) { if (i == 46) //[.] { keyCode = 0; _keyMap[i] = (byte)keyCode; _reverseKeyMap[keyCode] = (byte)i; } else if (i == 42) //[*] { keyCode = 1; _keyMap[i] = 0xff; //skipped value for optimization _reverseKeyMap[keyCode] = (byte)i; } else if (i == 45) //[-] { keyCode = 2; _keyMap[i] = (byte)keyCode; _reverseKeyMap[keyCode] = (byte)i; } else if (i == 47) //[/] { keyCode = 3; _keyMap[i] = (byte)keyCode; _reverseKeyMap[keyCode] = (byte)i; } else if ((i >= 48) && (i <= 57)) //[0-9] { keyCode = i - 44; //4 - 13 _keyMap[i] = (byte)keyCode; _reverseKeyMap[keyCode] = (byte)i; } else if (i == 95) //[_] { keyCode = 14; _keyMap[i] = (byte)keyCode; _reverseKeyMap[keyCode] = (byte)i; } else if ((i >= 97) && (i <= 122)) //[a-z] { keyCode = i - 82; //15 - 40 _keyMap[i] = (byte)keyCode; _reverseKeyMap[keyCode] = (byte)i; } else if ((i >= 65) && (i <= 90)) //[A-Z] { keyCode = i - 50; //15 - 40 _keyMap[i] = (byte)keyCode; } else { _keyMap[i] = 0xff; } } } public DomainTree() : base(41) { } #endregion #region protected protected override byte[] ConvertToByteKey(string domain, bool throwException = true) { if (domain.Length == 0) return []; if (domain.Length > 255) { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: length cannot exceed 255 bytes."); return null; } byte[] key = new byte[domain.Length + 1]; int keyOffset = 0; int labelStart; int labelEnd = domain.Length - 1; int labelLength; int labelChar; byte labelKeyCode; int i; do { if (labelEnd < 0) labelEnd = 0; labelStart = domain.LastIndexOf('.', labelEnd); labelLength = labelEnd - labelStart; if (labelLength == 0) { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: label length cannot be 0 byte."); return null; } if (labelLength > 63) { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: label length cannot exceed 63 bytes."); return null; } if (domain[labelStart + 1] == '-') { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: label cannot start with hyphen."); return null; } if (domain[labelEnd] == '-') { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: label cannot end with hyphen."); return null; } if ((labelLength == 1) && (domain[labelStart + 1] == '*')) //[*] { key[keyOffset++] = 1; } else { for (i = labelStart + 1; i <= labelEnd; i++) { labelChar = domain[i]; if (labelChar >= _keyMap.Length) { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: invalid character [" + labelChar + "] was found."); return null; } labelKeyCode = _keyMap[labelChar]; if (labelKeyCode == 0xff) { if (throwException) throw new InvalidDomainNameException("Invalid domain name [" + domain + "]: invalid character [" + labelChar + "] was found."); return null; } key[keyOffset++] = labelKeyCode; } } key[keyOffset++] = 0; //[.] labelEnd = labelStart - 1; } while (labelStart > -1); return key; } protected static string ConvertKeyToLabel(byte[] key, int startIndex) { int length = key.Length - startIndex; if (length < 1) return null; Span domain = stackalloc byte[length]; int i; int k; for (i = 0; i < domain.Length; i++) { k = key[i + startIndex]; if (k == 0) //[.] break; domain[i] = _reverseKeyMap[k]; } return Encoding.ASCII.GetString(domain.Slice(0, i)); } #endregion #region public public override bool TryRemove(string key, out T value) { if (TryRemove(key, out value, out Node currentNode)) { currentNode.CleanThisBranch(); return true; } return false; } #endregion } } ================================================ FILE: DnsServerCore/Dns/Trees/InvalidDomainNameException.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; namespace DnsServerCore.Dns.Trees { public class InvalidDomainNameException : DnsServerException { #region constructors public InvalidDomainNameException() : base() { } public InvalidDomainNameException(string message) : base(message) { } public InvalidDomainNameException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore/Dns/Trees/ZoneTree.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.Dns.Zones; using System.Collections.Generic; using System.Threading; namespace DnsServerCore.Dns.Trees { abstract class ZoneTree : DomainTree where TNode : class where TSubDomainZone : Zone where TApexZone : Zone { #region private private static Node GetNextChildZoneNode(Node current, int baseDepth) { int k = 0; while ((current is not null) && (current.Depth >= baseDepth)) { if ((current.K != 0) || (current.Depth == baseDepth)) //[.] skip this node's children as its last for current sub zone { Node[] children = current.Children; if (children is not null) { //find child node Node child = null; for (int i = k; i < children.Length; i++) { child = Volatile.Read(ref children[i]); if (child is not null) { if (child.Value is not null) return child; //child has value so return it if (child.K == 0) //[.] return child; //child node is last for current sub zone if (child.Children is not null) break; } } if (child is not null) { //make found child as current k = 0; current = child; continue; //start over } } } //no child nodes available; move up to parent node k = current.K + 1; current = current.Parent; } return null; } private static byte[] GetNodeKey(Node node) { byte[] key = new byte[node.Depth]; int i = node.Depth - 1; while (i > -1) { key[i--] = node.K; node = node.Parent; } return key; } private static bool KeysMatch(byte[] mainKey, byte[] testKey, bool matchWildcard) { if (matchWildcard) { //com.example.*. //com.example.*.www. //com.example.abc.www. int i = 0; int j = 0; while ((i < mainKey.Length) && (j < testKey.Length)) { if ((mainKey[i] == 1) && (testKey[j] != 1)) //[*] wildcard match only when test key does not have '*' as literal char: RFC 4592 section 2.3 { if (i == mainKey.Length - 2) return true; //last label, valid wildcard } if (mainKey[i] != testKey[j]) return false; i++; j++; } return (i == mainKey.Length) && (j == testKey.Length); } else { //exact match if (mainKey.Length != testKey.Length) return false; for (int i = 0; i < mainKey.Length; i++) { if (mainKey[i] != testKey[i]) return false; } return true; } } private void FindClosestValuesForZone(TNode zoneNode, Node currentNode, ref Node closestSubDomainNode, ref Node closestAuthorityNode, ref TSubDomainZone closestSubDomain, ref TSubDomainZone closestDelegation, ref TApexZone closestAuthority) { GetClosestValuesForZone(zoneNode, out TSubDomainZone subDomain, out TSubDomainZone delegation, out TApexZone authority); if (subDomain is not null) { closestSubDomain = subDomain; closestSubDomainNode = currentNode; } if (delegation is not null) closestDelegation = delegation; if (authority is not null) { closestAuthority = authority; closestAuthorityNode = currentNode; closestSubDomain = null; //clear previous closest sub domain closestSubDomainNode = null; } } #endregion #region protected protected static bool IsKeySubDomain(byte[] mainKey, byte[] testKey, bool matchWildcard) { if (matchWildcard) { //com.example.*. //com.example.*.www. //com.example.abc.www. int i = 0; int j = 0; while ((i < mainKey.Length) && (j < testKey.Length)) { if ((mainKey[i] == 1) && (testKey[j] != 1)) //[*] wildcard match only when test key does not have '*' as literal char: RFC 4592 section 2.3 { //skip j to next label while (j < testKey.Length) { if (testKey[j] == 0) //[.] break; j++; } i++; continue; } if (mainKey[i] != testKey[j]) return false; i++; j++; } return (i == mainKey.Length) && (j < testKey.Length); } else { //exact match if (mainKey.Length > testKey.Length) return false; for (int i = 0; i < mainKey.Length; i++) { if (mainKey[i] != testKey[i]) return false; } return mainKey.Length < testKey.Length; } } protected TNode FindZoneNode(byte[] key, bool matchWildcard, out Node currentNode, out Node closestSubDomainNode, out Node closestAuthorityNode, out TSubDomainZone closestSubDomain, out TSubDomainZone closestDelegation, out TApexZone closestAuthority) { currentNode = _root; closestSubDomainNode = null; closestAuthorityNode = null; closestSubDomain = null; closestDelegation = null; closestAuthority = null; Node wildcardNode = null; int i = 0; while (i <= key.Length) { //inspect the current node NodeValue value = currentNode.Value; if ((value is not null) && (value.Key.Length <= key.Length)) { TNode zoneNode = value.Value; if ((zoneNode is not null) && IsKeySubDomain(value.Key, key, matchWildcard)) { FindClosestValuesForZone(zoneNode, currentNode, ref closestSubDomainNode, ref closestAuthorityNode, ref closestSubDomain, ref closestDelegation, ref closestAuthority); wildcardNode = null; //clear previous wildcard node } } if (i == key.Length) break; Node[] children = currentNode.Children; if (children is null) break; Node childNode; if (matchWildcard && (key[i] != 1)) //wildcard match only when key does not have '*' as literal char: RFC 4592 section 2.3 { childNode = Volatile.Read(ref children[1]); //[*] if (childNode is not null) { NodeValue wValue = childNode.Value; if (wValue is null) { //find value from next [.] node Node[] wChildren = childNode.Children; if (wChildren is not null) { Node wChildNode = Volatile.Read(ref wChildren[0]); //[.] if (wChildNode is not null) { wValue = wChildNode.Value; if ((wValue is not null) && (wValue.Key.Length == wChildNode.Depth)) wildcardNode = wChildNode; } } } else if (wValue.Key.Length == childNode.Depth + 1) { wildcardNode = childNode; } } } childNode = Volatile.Read(ref children[key[i]]); if (childNode is null) { //no child found if (wildcardNode is null) return null; //no child or wildcard found //use wildcard node break; } currentNode = childNode; i++; } { NodeValue value = currentNode.Value; if (value is not null) { //match exact only if (KeysMatch(value.Key, key, matchWildcard)) { //find closest values since the matched zone may be apex zone TNode zoneNode = value.Value; if (zoneNode is not null) FindClosestValuesForZone(zoneNode, currentNode, ref closestSubDomainNode, ref closestAuthorityNode, ref closestSubDomain, ref closestDelegation, ref closestAuthority); return value.Value; //found matching value } if (wildcardNode is not null) { NodeValue wildcardValue = wildcardNode.Value; if (wildcardValue is not null) { if (IsKeySubDomain(key, value.Key, false) && IsKeySubDomain(wildcardValue.Key, value.Key, matchWildcard)) { //value is a subdomain of an ENT so wildcard is not valid wildcardNode = null; } } } } else if ((wildcardNode is not null) && (currentNode.K == 0) && currentNode.HasChildren && (currentNode != wildcardNode.Parent)) { //ENT node with children so wildcard is not valid wildcardNode = null; } } if (wildcardNode is not null) { //inspect wildcard node value NodeValue value = wildcardNode.Value; if (value is not null) { //match wildcard keys if (KeysMatch(value.Key, key, true)) { //find closest values TNode zoneNode = value.Value; if (zoneNode is not null) FindClosestValuesForZone(zoneNode, currentNode, ref closestSubDomainNode, ref closestAuthorityNode, ref closestSubDomain, ref closestDelegation, ref closestAuthority); return value.Value; //found matching wildcard value } } } //value not found return null; } protected abstract void GetClosestValuesForZone(TNode zoneValue, out TSubDomainZone closestSubDomain, out TSubDomainZone closestDelegation, out TApexZone closestAuthority); #endregion #region public public void ListSubDomains(string domain, List subDomains) { byte[] bKey = ConvertToByteKey(domain); _ = _root.FindNodeValue(bKey, out Node currentNode); Node current = currentNode; NodeValue value; do { value = current.Value; if (value is not null) { if (IsKeySubDomain(bKey, value.Key, false)) { string label = ConvertKeyToLabel(value.Key, bKey.Length); if (label is not null) subDomains.Add(label); } } else if ((current.K == 0) && (current.Depth > currentNode.Depth)) //[.] { byte[] nodeKey = GetNodeKey(current); if (IsKeySubDomain(bKey, nodeKey, false)) { string label = ConvertKeyToLabel(nodeKey, bKey.Length); if (label is not null) subDomains.Add(label); } } current = GetNextChildZoneNode(current, currentNode.Depth); } while (current is not null); } #endregion } } ================================================ FILE: DnsServerCore/Dns/ZoneManagers/AllowedZoneManager.cs ================================================ /* 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.Dns.ResourceRecords; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ZoneManagers { public sealed class AllowedZoneManager : IDisposable { #region variables readonly DnsServer _dnsServer; AuthZoneManager _zoneManager; readonly DnsSOARecordDataExtended _soaRecord; readonly DnsNSRecordDataExtended _nsRecord; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor public AllowedZoneManager(DnsServer dnsServer) { _dnsServer = dnsServer; _zoneManager = new AuthZoneManager(_dnsServer); _soaRecord = new DnsSOARecordDataExtended(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 900, 300, 604800, 60); _nsRecord = new DnsNSRecordDataExtended(_dnsServer.ServerDomain); _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { if (_pendingSave) { try { SaveZoneFileInternal(); _pendingSave = false; } catch (Exception ex) { _dnsServer.LogManager.Write(ex); //set timer to retry again _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveZoneFileInternal(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { _pendingSave = false; } } } _disposed = true; } #endregion #region zone file public void LoadAllowedZoneFile() { string allowedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "allowed.config"); try { using (FileStream fS = new FileStream(allowedZoneFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS); } _dnsServer.LogManager.Write("DNS Server allowed zone file was loaded: " + allowedZoneFile); } catch (FileNotFoundException) { SaveZoneFileInternal(); } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server encountered an error while loading allowed zone file: " + allowedZoneFile + "\r\n" + ex.ToString()); } } public void LoadAllowedZone(Stream s) { lock (_saveLock) { ReadConfigFrom(s); SaveZoneFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void SaveZoneFileInternal() { string allowedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "allowed.config"); using (FileStream fS = new FileStream(allowedZoneFile, FileMode.Create, FileAccess.Write)) { WriteConfigTo(fS); } _dnsServer.LogManager.Write("DNS Server allowed zone file was saved: " + allowedZoneFile); } public void SaveZoneFile() { lock (_saveLock) { if (_pendingSave) return; _pendingSave = true; _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void ReadConfigFrom(Stream s) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "AZ") //format throw new InvalidDataException("DnsServer allowed zone file format is invalid."); byte version = bR.ReadByte(); switch (version) { case 1: int length = bR.ReadInt32(); int i = 0; AuthZoneManager zoneManager = new AuthZoneManager(_dnsServer); zoneManager.LoadSpecialPrimaryZones(delegate () { if (i++ < length) return bR.ReadShortString(); return null; }, _soaRecord, _nsRecord); _zoneManager = zoneManager; break; default: throw new InvalidDataException("DnsServer allowed zone file version not supported."); } } private void WriteConfigTo(Stream s) { IReadOnlyList allowedZones = _zoneManager.GetAllZones(); BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("AZ")); //format bW.Write((byte)1); //version bW.Write(allowedZones.Count); foreach (AuthZoneInfo zone in allowedZones) bW.WriteShortString(zone.Name); } #endregion #region private internal void UpdateServerDomain() { _soaRecord.UpdatePrimaryNameServerAndMinimum(_dnsServer.ServerDomain, _dnsServer.BlockingAnswerTtl); _nsRecord.UpdateNameServer(_dnsServer.ServerDomain); } #endregion #region public public void ImportZones(string[] domains) { _zoneManager.LoadSpecialPrimaryZones(domains, _soaRecord, _nsRecord); } public bool AllowZone(string domain) { if (_zoneManager.CreateSpecialPrimaryZone(domain, _soaRecord, _nsRecord) != null) return true; return false; } public bool DeleteZone(string domain) { if (_zoneManager.DeleteZone(domain)) return true; return false; } public void Flush() { _zoneManager.Flush(); } public IReadOnlyList GetAllZones() { return _zoneManager.GetAllZones(); } public void ListAllRecords(string domain, List records) { _zoneManager.ListAllRecords(domain, domain, records); } public void ListSubDomains(string domain, List subDomains) { _zoneManager.ListSubDomains(domain, subDomains); } public bool IsAllowed(DnsDatagram request) { if (_zoneManager.TotalZones < 1) return false; return _zoneManager.Query(request, false) is not null; } #endregion #region properties public int TotalZonesAllowed { get { return _zoneManager.TotalZones; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs ================================================ /* 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.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ZoneManagers { public sealed class AuthZoneManager : IDisposable { #region events public event EventHandler SecondaryCatalogZoneAdded; public event EventHandler SecondaryCatalogZoneRemoved; #endregion #region variables readonly DnsServer _dnsServer; string _serverDomain; uint _defaultRecordTtl = 3600; uint _defaultNsRecordTtl = 14400; uint _defaultSoaRecordTtl = 900; bool _useSoaSerialDateScheme; uint _minSoaRefresh = 300; uint _minSoaRetry = 300; readonly AuthZoneTree _root = new AuthZoneTree(); readonly List _zoneIndex = new List(10); readonly List _catalogZoneIndex = new List(2); readonly ReaderWriterLockSlim _zoneIndexLock = new ReaderWriterLockSlim(); readonly object _saveLock = new object(); readonly Dictionary _pendingSaveZones = new Dictionary(); readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; volatile int _updateServerDomainId; #endregion #region constructor public AuthZoneManager(DnsServer dnsServer) { _dnsServer = dnsServer; _serverDomain = _dnsServer.ServerDomain; _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { List failedZones = new List(); foreach (KeyValuePair pendingSaveZone in _pendingSaveZones) { try { SaveZoneFileInternal(pendingSaveZone.Key); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); failedZones.Add(pendingSaveZone.Key); } } _pendingSaveZones.Clear(); foreach (string zoneName in failedZones) _pendingSaveZones.TryAdd(zoneName, null); if (_pendingSaveZones.Count > 0) _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } }); } #endregion #region IDisposable bool _disposed; private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { lock (_saveLock) { _saveTimer?.Dispose(); try { foreach (KeyValuePair pendingSaveZone in _pendingSaveZones) { try { SaveZoneFileInternal(pendingSaveZone.Key); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } } finally { _pendingSaveZones.Clear(); } } foreach (AuthZoneNode zoneNode in _root) zoneNode.Dispose(); _zoneIndexLock.Dispose(); } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region zone file serialization and loading public void LoadAllZoneFiles() { string zonesFolder = Path.Combine(_dnsServer.ConfigFolder, "zones"); if (!Directory.Exists(zonesFolder)) Directory.CreateDirectory(zonesFolder); //move zone files to new folder { string[] oldZoneFiles = Directory.GetFiles(_dnsServer.ConfigFolder, "*.zone"); foreach (string oldZoneFile in oldZoneFiles) File.Move(oldZoneFile, Path.Combine(zonesFolder, Path.GetFileName(oldZoneFile))); } //remove old internal zones files { string[] oldZoneFiles = ["localhost.zone", "1.0.0.127.in-addr.arpa.zone", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.zone"]; foreach (string oldZoneFile in oldZoneFiles) { string filePath = Path.Combine(zonesFolder, oldZoneFile); if (File.Exists(filePath)) { try { File.Delete(filePath); } catch { } } } } //flush existing zones Flush(); //load all internal zones LoadAllInternalZones(); //load zone files _zoneIndexLock.EnterWriteLock(); try { string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone"); foreach (string zoneFile in zoneFiles) { try { using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) { AuthZoneInfo zoneInfo = LoadZoneFrom(fS, File.GetLastWriteTimeUtc(fS.SafeFileHandle)); _zoneIndex.Add(zoneInfo); if (zoneInfo.Type == AuthZoneType.Catalog) _catalogZoneIndex.Add(zoneInfo); } _dnsServer.LogManager.Write("DNS Server successfully loaded zone file: " + zoneFile); } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString()); } } _zoneIndex.Sort(); _catalogZoneIndex.Sort(); } finally { _zoneIndexLock.ExitWriteLock(); } } private void LoadAllInternalZones() { { CreateInternalPrimaryZone("localhost"); SetRecord("localhost", new DnsResourceRecord("localhost", DnsResourceRecordType.A, DnsClass.IN, 3600, new DnsARecordData(IPAddress.Loopback))); SetRecord("localhost", new DnsResourceRecord("localhost", DnsResourceRecordType.AAAA, DnsClass.IN, 3600, new DnsAAAARecordData(IPAddress.IPv6Loopback))); } { string ptrDomain = "0.in-addr.arpa"; CreateInternalPrimaryZone(ptrDomain); } { string ptrDomain = "255.in-addr.arpa"; CreateInternalPrimaryZone(ptrDomain); } { string ptrZoneName = "127.in-addr.arpa"; CreateInternalPrimaryZone(ptrZoneName); SetRecord(ptrZoneName, new DnsResourceRecord("1.0.0.127.in-addr.arpa", DnsResourceRecordType.PTR, DnsClass.IN, 3600, new DnsPTRRecordData("localhost"))); } { string ptrZoneName = IPAddress.IPv6Loopback.GetReverseDomain(); CreateInternalPrimaryZone(ptrZoneName); SetRecord(ptrZoneName, new DnsResourceRecord(ptrZoneName, DnsResourceRecordType.PTR, DnsClass.IN, 3600, new DnsPTRRecordData("localhost"))); } } private void SaveZoneFileInternal(string zoneName) { zoneName = zoneName.ToLowerInvariant(); using (MemoryStream mS = new MemoryStream()) { //serialize zone WriteZoneTo(zoneName, mS); if (mS.Position == 0) return; //zone was not found //write to zone file mS.Position = 0; using (FileStream fS = new FileStream(Path.Combine(_dnsServer.ConfigFolder, "zones", zoneName + ".zone"), FileMode.Create, FileAccess.Write)) { mS.CopyTo(fS); } } _dnsServer.LogManager.Write("Saved zone file for domain: " + (zoneName == "" ? "" : zoneName)); } public void SaveZoneFile(string zoneName) { zoneName = zoneName.ToLowerInvariant(); lock (_saveLock) { if (!_pendingSaveZones.TryAdd(zoneName, null)) return; if (_pendingSaveZones.Count == 1) _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private static uint GetMinExpiryTtlFor(IReadOnlyList records) { uint minExpiryTtl = 0u; foreach (DnsResourceRecord record in records) { GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); if (recordInfo.ExpiryTtl > 0u) { uint pendingExpiryTtl = recordInfo.GetPendingExpiryTtl(); if (pendingExpiryTtl == 0) { //expired record found; set 10 sec ttl for timer to delete it minExpiryTtl = 10; } else { if (minExpiryTtl == 0u) minExpiryTtl = pendingExpiryTtl; else minExpiryTtl = Math.Min(minExpiryTtl, pendingExpiryTtl); } } } return minExpiryTtl; } private void LoadAndInitZone(AuthZoneInfo zoneInfo, IReadOnlyList records) { ApexZone apexZone = zoneInfo.ApexZone; //load records foreach (KeyValuePair>> zoneEntry in DnsResourceRecord.GroupRecords(records)) { if (apexZone.Name.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) apexZone.LoadRecords(rrsetEntry.Key, rrsetEntry.Value); } else { ValidateIfDomainBelongsToZone(apexZone.Name, zoneEntry.Key); AuthZone authZone = GetOrAddSubDomainZone(apexZone.Name, zoneEntry.Key); foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) authZone.LoadRecords(rrsetEntry.Key, rrsetEntry.Value); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } } //update dnssec status apexZone.UpdateDnssecStatus(); //init zone switch (zoneInfo.Type) { case AuthZoneType.Primary: { apexZone.TriggerNotify(); uint minExpiryTtl = GetMinExpiryTtlFor(records); if (minExpiryTtl > 0u) apexZone.StartRecordExpiryTimer(minExpiryTtl); } break; case AuthZoneType.Secondary: { SecondaryZone secondary = apexZone as SecondaryZone; DnsResourceRecord soaRecord = secondary.GetRecords(DnsResourceRecordType.SOA)[0]; SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo(); if (soaInfo.Version == 1) { secondary.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers; secondary.PrimaryZoneTransferProtocol = soaInfo.ZoneTransferProtocol; secondary.PrimaryZoneTransferTsigKeyName = soaInfo.TsigKeyName; } secondary.TriggerNotify(); secondary.TriggerRefresh(); } break; case AuthZoneType.Stub: { StubZone stub = apexZone as StubZone; DnsResourceRecord soaRecord = stub.GetRecords(DnsResourceRecordType.SOA)[0]; SOARecordInfo soaInfo = soaRecord.GetAuthSOARecordInfo(); if (soaInfo.Version == 1) stub.PrimaryNameServerAddresses = soaInfo.PrimaryNameServers; stub.TriggerRefresh(); } break; case AuthZoneType.Forwarder: { IReadOnlyList soaRecords = apexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count == 0) (apexZone as ForwarderZone).InitZone(); apexZone.TriggerNotify(); uint minExpiryTtl = GetMinExpiryTtlFor(records); if (minExpiryTtl > 0u) apexZone.StartRecordExpiryTimer(minExpiryTtl); } break; case AuthZoneType.SecondaryForwarder: { (apexZone as SecondaryZone).TriggerRefresh(); } break; case AuthZoneType.Catalog: { (apexZone as CatalogZone).BuildMembersIndex(); apexZone.TriggerNotify(); uint minExpiryTtl = GetMinExpiryTtlFor(records); if (minExpiryTtl > 0u) apexZone.StartRecordExpiryTimer(minExpiryTtl); } break; case AuthZoneType.SecondaryCatalog: { (apexZone as SecondaryZone).TriggerRefresh(); (apexZone as SecondaryCatalogZone).BuildMembersIndex(); } break; } } public AuthZoneInfo LoadZoneFrom(Stream s, DateTime lastModified) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DZ") throw new InvalidDataException("DnsServer zone file format is invalid."); switch (bR.ReadByte()) { case 2: { DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; if (records.Length == 0) throw new InvalidDataException("Zone does not contain SOA record."); DnsResourceRecord soaRecord = null; for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); if (records[i].Type == DnsResourceRecordType.SOA) soaRecord = records[i]; } if (soaRecord == null) throw new InvalidDataException("Zone does not contain SOA record."); //make zone info AuthZoneType zoneType; if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) zoneType = AuthZoneType.Primary; else zoneType = AuthZoneType.Stub; AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, false); //create zone ApexZone apexZone = CreateEmptyApexZone(zoneInfo); zoneInfo = new AuthZoneInfo(apexZone); try { //load and init zone LoadAndInitZone(zoneInfo, records); } catch { DeleteZone(zoneInfo); throw; } return zoneInfo; } case 3: { bool zoneDisabled = bR.ReadBoolean(); DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; if (records.Length == 0) throw new InvalidDataException("Zone does not contain SOA record."); DnsResourceRecord soaRecord = null; for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); records[i].Tag = AuthRecordInfo.ReadGenericRecordInfoFrom(bR, records[i].Type); if (records[i].Type == DnsResourceRecordType.SOA) soaRecord = records[i]; } if (soaRecord == null) throw new InvalidDataException("Zone does not contain SOA record."); //make zone info AuthZoneType zoneType; if (_dnsServer.ServerDomain.Equals((soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer, StringComparison.OrdinalIgnoreCase)) zoneType = AuthZoneType.Primary; else zoneType = AuthZoneType.Stub; AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, zoneType, zoneDisabled); //create zone ApexZone apexZone = CreateEmptyApexZone(zoneInfo); zoneInfo = new AuthZoneInfo(apexZone); try { //load and init zone LoadAndInitZone(zoneInfo, records); } catch { DeleteZone(zoneInfo); throw; } return zoneInfo; } case 4: { //read zone info AuthZoneInfo zoneInfo = new AuthZoneInfo(bR, lastModified); //create zone ApexZone apexZone = CreateEmptyApexZone(zoneInfo); zoneInfo = new AuthZoneInfo(apexZone); try { //read all zone records DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; if (records.Length < 1) throw new InvalidDataException("Failed to load DNS zone file: the zone file does not contain any records."); for (int i = 0; i < records.Length; i++) { records[i] = new DnsResourceRecord(s); records[i].Tag = AuthRecordInfo.ReadGenericRecordInfoFrom(bR, records[i].Type); } //load and init zone LoadAndInitZone(zoneInfo, records); } catch { DeleteZone(zoneInfo); throw; } return zoneInfo; } default: throw new InvalidDataException("DNS Zone file version not supported."); } } public void WriteZoneTo(string zoneName, Stream s) { AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName, true); if (zoneInfo is null) return; //serialize zone BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("DZ")); //format bW.Write((byte)4); //version //write zone info if (zoneInfo.Internal) throw new InvalidOperationException("Cannot save zones marked as internal."); zoneInfo.WriteTo(bW); //write all zone records List records = new List(); ListAllZoneRecords(zoneInfo.Name, records); bW.Write(records.Count); foreach (DnsResourceRecord record in records) { record.WriteTo(s); record.GetAuthGenericRecordInfo().WriteTo(bW); } } #endregion #region internal internal void TriggerUpdateServerDomain(bool useBlockingAnswerTtl = false) { int id = RandomNumberGenerator.GetInt32(int.MaxValue); _updateServerDomainId = id; ThreadPool.QueueUserWorkItem(delegate (object state) { string serverDomain = _dnsServer.ServerDomain; //update authoritative zone SOA and NS records try { IReadOnlyList zones = GetAllZones(); foreach (AuthZoneInfo zone in zones) { if (_updateServerDomainId != id) return; //stop current update since another update has been triggerred if (zone.Type != AuthZoneType.Primary) continue; DnsResourceRecord record = zone.ApexZone.GetRecords(DnsResourceRecordType.SOA)[0]; DnsSOARecordData soa = record.RDATA as DnsSOARecordData; uint ttl; uint minimum; if (useBlockingAnswerTtl) { ttl = _dnsServer.BlockingAnswerTtl; minimum = ttl; } else { ttl = record.TTL; minimum = soa.Minimum; } if (soa.PrimaryNameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) { SetRecord(zone.Name, new DnsResourceRecord(record.Name, record.Type, DnsClass.IN, ttl, new DnsSOARecordData(serverDomain, soa.ResponsiblePerson, soa.Serial, soa.Refresh, soa.Retry, soa.Expire, minimum))); //update NS records IReadOnlyList nsResourceRecords = zone.ApexZone.GetRecords(DnsResourceRecordType.NS); foreach (DnsResourceRecord nsResourceRecord in nsResourceRecords) { if ((nsResourceRecord.RDATA as DnsNSRecordData).NameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) { UpdateRecord(zone.Name, nsResourceRecord, new DnsResourceRecord(nsResourceRecord.Name, nsResourceRecord.Type, nsResourceRecord.Class, nsResourceRecord.TTL, new DnsNSRecordData(serverDomain)) { Tag = nsResourceRecord.Tag }); break; } } if (zone.Internal) continue; //dont save internal zones to disk //save zone file SaveZoneFile(zone.Name); } } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } //update server domain _serverDomain = serverDomain; }); } internal static string GetParentZone(string domain) { int i = domain.IndexOf('.'); if (i > -1) return domain.Substring(i + 1); //dont return root zone return null; } internal static bool DomainBelongsToZone(string zoneName, string domain) { return domain.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || domain.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase) || (zoneName.Length == 0); } internal static void ValidateIfDomainBelongsToZone(string zoneName, string domain) { if (!DomainBelongsToZone(zoneName, domain)) throw new DnsServerException("The domain name '" + domain + "' does not belong to the zone: " + zoneName); } #endregion #region auth zone tree methods private ApexZone CreateEmptyApexZone(AuthZoneInfo zoneInfo) { ApexZone apexZone; switch (zoneInfo.Type) { case AuthZoneType.Primary: apexZone = new PrimaryZone(_dnsServer, zoneInfo); break; case AuthZoneType.Secondary: apexZone = new SecondaryZone(_dnsServer, zoneInfo); break; case AuthZoneType.Stub: apexZone = new StubZone(_dnsServer, zoneInfo); break; case AuthZoneType.Forwarder: apexZone = new ForwarderZone(_dnsServer, zoneInfo); break; case AuthZoneType.SecondaryForwarder: apexZone = new SecondaryForwarderZone(_dnsServer, zoneInfo); break; case AuthZoneType.Catalog: apexZone = new CatalogZone(_dnsServer, zoneInfo); break; case AuthZoneType.SecondaryCatalog: SecondaryCatalogZone secondaryCatalogZone = new SecondaryCatalogZone(_dnsServer, zoneInfo); secondaryCatalogZone.ZoneAdded += SecondaryCatalogZoneAdded; secondaryCatalogZone.ZoneRemoved += SecondaryCatalogZoneRemoved; apexZone = secondaryCatalogZone; break; default: throw new InvalidDataException("DNS zone type not supported."); } if (_root.TryAdd(apexZone)) return apexZone; throw new DnsServerException("Zone already exists: " + zoneInfo.DisplayName); } internal AuthZone GetOrAddSubDomainZone(string zoneName, string domain) { return _root.GetOrAddSubDomainZone(zoneName, domain, delegate () { if (!_root.TryGet(zoneName, out ApexZone apexZone)) throw new DnsServerException("Zone was not found for domain: " + domain); if (apexZone is PrimaryZone primaryZone) return new PrimarySubDomainZone(primaryZone, domain); else if (apexZone is SecondaryCatalogZone secondaryCatalogZone) return new SecondaryCatalogSubDomainZone(secondaryCatalogZone, domain); else if (apexZone is SecondaryZone secondaryZone) return new SecondarySubDomainZone(secondaryZone, domain); else if (apexZone is CatalogZone catalogZone) return new CatalogSubDomainZone(catalogZone, domain); else if (apexZone is ForwarderZone forwarderZone) return new ForwarderSubDomainZone(forwarderZone, domain); throw new DnsServerException("Zone cannot have sub domains."); }); } internal IReadOnlyList GetApexZoneWithSubDomainZones(string zoneName) { return _root.GetApexZoneWithSubDomainZones(zoneName); } public AuthZoneInfo GetAuthZoneInfo(string zoneName, bool loadHistory = false) { if (_root.TryGet(zoneName, out AuthZoneNode authZoneNode) && (authZoneNode.ApexZone is not null)) return new AuthZoneInfo(authZoneNode.ApexZone, loadHistory); return null; } public AuthZoneInfo FindAuthZoneInfo(string domain, bool loadHistory = false) { _ = _root.FindZone(domain, out _, out _, out ApexZone apexZone, out _); if (apexZone is null) return null; return new AuthZoneInfo(apexZone, loadHistory); } internal AuthZone GetAuthZone(string zoneName, string domain) { return _root.GetAuthZone(zoneName, domain); } internal ApexZone GetApexZone(string zoneName) { return _root.GetApexZone(zoneName); } public bool NameExists(string zoneName, string domain) { ValidateIfDomainBelongsToZone(zoneName, domain); return _root.TryGet(zoneName, domain, out _); } internal AuthZone FindPreviousSubDomainZone(string zoneName, string domain) { return _root.FindPreviousSubDomainZone(zoneName, domain); } internal AuthZone FindNextSubDomainZone(string zoneName, string domain) { return _root.FindNextSubDomainZone(zoneName, domain); } public void ListSubDomains(string domain, List subDomains) { _root.ListSubDomains(domain, subDomains); } internal bool SubDomainExistsFor(string zoneName, string domain) { return _root.SubDomainExistsFor(zoneName, domain); } internal void RemoveSubDomainZone(string domain, bool removeAllSubDomains = false) { _root.TryRemove(domain, out SubDomainZone _, removeAllSubDomains); } internal void Flush() { _zoneIndexLock.EnterWriteLock(); try { foreach (AuthZoneNode zoneNode in _root) zoneNode.Dispose(); _root.Clear(); _zoneIndex.Clear(); _catalogZoneIndex.Clear(); } finally { _zoneIndexLock.ExitWriteLock(); } } #endregion #region zone create / delete / convert / clone internal AuthZoneInfo CreateSpecialPrimaryZone(string zoneName, DnsSOARecordData soaRecord, DnsNSRecordData ns) { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } internal void LoadSpecialPrimaryZones(IReadOnlyList zoneNames, DnsSOARecordData soaRecord, DnsNSRecordData ns) { _zoneIndexLock.EnterWriteLock(); try { foreach (string zoneName in zoneNames) { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); } } _zoneIndex.Sort(); } finally { _zoneIndexLock.ExitWriteLock(); } } internal void LoadSpecialPrimaryZones(Func getZoneName, DnsSOARecordData soaRecord, DnsNSRecordData ns) { _zoneIndexLock.EnterWriteLock(); try { string zoneName; while (true) { zoneName = getZoneName(); if (zoneName is null) break; PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, soaRecord, ns); if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); } } _zoneIndex.Sort(); } finally { _zoneIndexLock.ExitWriteLock(); } } internal AuthZoneInfo CreateInternalPrimaryZone(string zoneName) { return CreatePrimaryZone(zoneName, true, _useSoaSerialDateScheme); } public AuthZoneInfo CreatePrimaryZone(string zoneName) { return CreatePrimaryZone(zoneName, false, _useSoaSerialDateScheme); } public AuthZoneInfo CreatePrimaryZone(string zoneName, bool useSoaSerialDateScheme) { return CreatePrimaryZone(zoneName, false, useSoaSerialDateScheme); } private AuthZoneInfo CreatePrimaryZone(string zoneName, bool @internal, bool useSoaSerialDateScheme) { PrimaryZone apexZone = new PrimaryZone(_dnsServer, zoneName, @internal, useSoaSerialDateScheme); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); if (!@internal) SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public Task CreateSecondaryZoneAsync(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateSecondaryZoneAsync(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, ignoreSoaFailure); } public async Task CreateSecondaryZoneAsync(string zoneName, IReadOnlyList primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false) { SecondaryZone apexZone = await SecondaryZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, ignoreSoaFailure); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public Task CreateStubZoneAsync(string zoneName, string primaryNameServerAddresses = null, bool ignoreSoaFailure = false) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateStubZoneAsync(zoneName, primaryNameServers, ignoreSoaFailure); } public async Task CreateStubZoneAsync(string zoneName, IReadOnlyList primaryNameServerAddresses = null, bool ignoreSoaFailure = false) { StubZone apexZone = await StubZone.CreateAsync(_dnsServer, zoneName, primaryNameServerAddresses, ignoreSoaFailure); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateForwarderZone(string zoneName) { ForwarderZone apexZone = new ForwarderZone(_dnsServer, zoneName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateForwarderZone(string zoneName, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments) { ForwarderZone apexZone = new ForwarderZone(_dnsServer, zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, fwdRecordComments); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateSecondaryForwarderZone(string zoneName, string primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateSecondaryForwarderZone(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); } public AuthZoneInfo CreateSecondaryForwarderZone(string zoneName, IReadOnlyList primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { SecondaryForwarderZone apexZone = new SecondaryForwarderZone(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateCatalogZone(string zoneName) { CatalogZone apexZone = new CatalogZone(_dnsServer, zoneName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); _catalogZoneIndex.Add(zoneInfo); _catalogZoneIndex.Sort(); apexZone.InitZoneProperties(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public AuthZoneInfo CreateSecondaryCatalogZone(string zoneName, string primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { NameServerAddress[] primaryNameServers; if (string.IsNullOrEmpty(primaryNameServerAddresses)) primaryNameServers = null; else primaryNameServers = primaryNameServerAddresses.Split(NameServerAddress.Parse, ','); return CreateSecondaryCatalogZone(zoneName, primaryNameServers, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); } public AuthZoneInfo CreateSecondaryCatalogZone(string zoneName, IReadOnlyList primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) { SecondaryCatalogZone apexZone = new SecondaryCatalogZone(_dnsServer, zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); _zoneIndexLock.EnterWriteLock(); try { if (_root.TryAdd(apexZone)) { apexZone.ZoneAdded += SecondaryCatalogZoneAdded; apexZone.ZoneRemoved += SecondaryCatalogZoneRemoved; apexZone.TriggerRefresh(0); AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); SaveZoneFile(zoneInfo.Name); return zoneInfo; } } finally { _zoneIndexLock.ExitWriteLock(); } return null; } public bool DeleteZone(string zoneName, bool deleteZoneFile = false) { AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName); if (zoneInfo is null) return false; return DeleteZone(zoneInfo, deleteZoneFile); } public bool DeleteZone(AuthZoneInfo zoneInfo, bool deleteZoneFile = false) { return DeleteZone(zoneInfo, deleteZoneFile, false); } private bool DeleteZone(AuthZoneInfo zoneInfo, bool deleteZoneFile, bool skipCatalogMemberZoneProcessing) { if (!skipCatalogMemberZoneProcessing) { switch (zoneInfo.Type) { case AuthZoneType.Catalog: //update all zone memberships for catalog zone to be deleted foreach (string memberZoneName in (zoneInfo.ApexZone as CatalogZone).GetAllMemberZoneNames()) { AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; if (zoneInfo.Name.Equals(memberZoneInfo.CatalogZoneName, StringComparison.OrdinalIgnoreCase)) { memberZoneInfo.ApexZone.CatalogZoneName = null; SaveZoneFile(memberZoneInfo.Name); } } break; case AuthZoneType.SecondaryCatalog: //delete all member zones for secondary catalog zone to be deleted foreach (string memberZoneName in (zoneInfo.ApexZone as SecondaryCatalogZone).GetAllMemberZoneNames()) { AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; if (zoneInfo.Name.Equals(memberZoneInfo.CatalogZoneName, StringComparison.OrdinalIgnoreCase)) DeleteZone(memberZoneInfo, true); } break; } } _zoneIndexLock.EnterWriteLock(); try { if (_root.TryRemove(zoneInfo.Name, out ApexZone removedApexZone)) { removedApexZone.Dispose(); _zoneIndex.Remove(zoneInfo); if (zoneInfo.Type == AuthZoneType.Catalog) _catalogZoneIndex.Remove(zoneInfo); if (zoneInfo.CatalogZoneName is not null) RemoveCatalogMemberZone(zoneInfo); //remove catalog zone membership if (deleteZoneFile) { File.Delete(Path.Combine(_dnsServer.ConfigFolder, "zones", zoneInfo.Name + ".zone")); _dnsServer.LogManager.Write("Deleted zone file for domain: " + zoneInfo.DisplayName); } return true; } } finally { _zoneIndexLock.ExitWriteLock(); } return false; } public AuthZoneInfo CloneZone(string zoneName, string sourceZoneName) { AuthZoneInfo sourceZoneInfo = GetAuthZoneInfo(sourceZoneName); if (sourceZoneInfo is null) throw new DnsServerException("No such zone was found: " + (sourceZoneName.Length == 0 ? "." : sourceZoneName)); AuthZoneInfo zoneInfo; switch (sourceZoneInfo.Type) { case AuthZoneType.Primary: zoneInfo = CreatePrimaryZone(zoneName); break; case AuthZoneType.Forwarder: zoneInfo = CreateForwarderZone(zoneName); break; default: throw new DnsServerException("Cannot clone the zone: source zone must be a Primary or Conditional Forwarder zone."); } if (zoneInfo is null) throw new DnsServerException("Failed to clone the zone: zone already exists."); //copy zone options zoneInfo.Disabled = sourceZoneInfo.Disabled; if (zoneInfo.Type == AuthZoneType.Primary) { zoneInfo.ZoneTransfer = sourceZoneInfo.ZoneTransfer; zoneInfo.ZoneTransferNetworkACL = sourceZoneInfo.ZoneTransferNetworkACL; zoneInfo.ZoneTransferTsigKeyNames = sourceZoneInfo.ZoneTransferTsigKeyNames; zoneInfo.Notify = sourceZoneInfo.Notify; zoneInfo.NotifyNameServers = sourceZoneInfo.NotifyNameServers; zoneInfo.Update = sourceZoneInfo.Update; zoneInfo.UpdateNetworkACL = sourceZoneInfo.UpdateNetworkACL; if (sourceZoneInfo.UpdateSecurityPolicies is not null) { Dictionary>> updateSecurityPolicies = new Dictionary>>(sourceZoneInfo.UpdateSecurityPolicies.Count); foreach (KeyValuePair>> sourceSecurityPolicy in sourceZoneInfo.UpdateSecurityPolicies) { Dictionary> policyMap = new Dictionary>(); foreach (KeyValuePair> sourcePolicyMap in sourceSecurityPolicy.Value) policyMap.Add(string.Concat(sourcePolicyMap.Key.AsSpan(0, sourcePolicyMap.Key.Length - sourceZoneName.Length), zoneName), sourcePolicyMap.Value); updateSecurityPolicies.Add(sourceSecurityPolicy.Key, policyMap); } zoneInfo.UpdateSecurityPolicies = updateSecurityPolicies; } } //copy records List sourceRecords = new List(); ListAllZoneRecords(sourceZoneName, sourceRecords); List newRecords = new List(sourceRecords.Count); foreach (DnsResourceRecord sourceRecord in sourceRecords) { switch (sourceRecord.Type) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.DS: continue; //skip DNSSEC records default: DnsResourceRecord newRecord = new DnsResourceRecord(string.Concat(sourceRecord.Name.AsSpan(0, sourceRecord.Name.Length - sourceZoneName.Length), zoneName), sourceRecord.Type, sourceRecord.Class, sourceRecord.TTL, sourceRecord.RDATA); if (sourceRecord.Tag is NSRecordInfo nsInfo) { NSRecordInfo nrInfo = new NSRecordInfo(); nrInfo.Disabled = nsInfo.Disabled; nrInfo.Comments = nsInfo.Comments; nrInfo.GlueRecords = nsInfo.GlueRecords; newRecord.Tag = nrInfo; } else if (sourceRecord.Tag is SOARecordInfo soaInfo) { SOARecordInfo nrInfo = new SOARecordInfo(); nrInfo.Disabled = soaInfo.Disabled; nrInfo.Comments = soaInfo.Comments; nrInfo.UseSoaSerialDateScheme = soaInfo.UseSoaSerialDateScheme; newRecord.Tag = nrInfo; } else if (sourceRecord.Tag is SVCBRecordInfo svcbInfo) { SVCBRecordInfo nrInfo = new SVCBRecordInfo(); nrInfo.Disabled = svcbInfo.Disabled; nrInfo.Comments = svcbInfo.Comments; nrInfo.AutoIpv4Hint = svcbInfo.AutoIpv4Hint; nrInfo.AutoIpv6Hint = svcbInfo.AutoIpv6Hint; newRecord.Tag = nrInfo; } else if (sourceRecord.Tag is GenericRecordInfo srInfo) { GenericRecordInfo nrInfo = new GenericRecordInfo(); nrInfo.Disabled = srInfo.Disabled; nrInfo.Comments = srInfo.Comments; newRecord.Tag = nrInfo; } newRecords.Add(newRecord); break; } } //load and init zone LoadAndInitZone(zoneInfo, newRecords); //save zone file SaveZoneFile(zoneInfo.Name); return zoneInfo; } public AuthZoneInfo ConvertZoneTypeTo(string zoneName, AuthZoneType newType) { AuthZoneInfo currentZoneInfo = GetAuthZoneInfo(zoneName); if (currentZoneInfo is null) throw new DnsServerException("No such zone was found: " + (zoneName.Length == 0 ? "." : zoneName)); //validate conversion type if (currentZoneInfo.Type == newType) throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone: the zone is already of the same type."); switch (currentZoneInfo.Type) { case AuthZoneType.Primary: switch (newType) { case AuthZoneType.Forwarder: if (currentZoneInfo.ApexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone: converting the zone will cause lose of DNSSEC private keys."); break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone: not supported."); } break; case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: switch (newType) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone: not supported."); } break; case AuthZoneType.Forwarder: switch (newType) { case AuthZoneType.Primary: break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone: not supported."); } break; default: throw new DnsServerException("Cannot convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone: not supported."); } return ConvertZoneTypeTo(currentZoneInfo, newType); } private AuthZoneInfo ConvertZoneTypeTo(AuthZoneInfo currentZoneInfo, AuthZoneType newType) { //read all current records List allRecords = new List(); ListAllZoneRecords(currentZoneInfo.Name, allRecords); try { //delete current zone from auth tree DeleteZone(currentZoneInfo, false, true); //create new zone AuthZoneInfo newZoneInfo; switch (newType) { case AuthZoneType.Primary: switch (currentZoneInfo.Type) { case AuthZoneType.Secondary: { //reset SOA metadata and remove DNSSEC records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { switch (record.Type) { case DnsResourceRecordType.SOA: { GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); record.Tag = null; GenericRecordInfo newRecordInfo = record.GetAuthGenericRecordInfo(); newRecordInfo.Comments = recordInfo.Comments; } break; case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: continue; } updateRecords.Add(record); } allRecords = updateRecords; } break; case AuthZoneType.Forwarder: case AuthZoneType.SecondaryForwarder: { //remove all FWD records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { if (record.Type == DnsResourceRecordType.FWD) continue; updateRecords.Add(record); } allRecords = updateRecords; } break; } newZoneInfo = CreatePrimaryZone(currentZoneInfo.Name); break; case AuthZoneType.Forwarder: switch (currentZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.SecondaryForwarder: { //remove SOA and NS records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { switch (record.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: continue; } updateRecords.Add(record); } allRecords = updateRecords; } break; case AuthZoneType.Secondary: { //remove SOA, NS and DNSSEC records List updateRecords = new List(allRecords.Count); foreach (DnsResourceRecord record in allRecords) { switch (record.Type) { case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.DS: continue; } updateRecords.Add(record); } allRecords = updateRecords; } break; } newZoneInfo = CreateForwarderZone(currentZoneInfo.Name); break; case AuthZoneType.Catalog: newZoneInfo = CreateCatalogZone(currentZoneInfo.Name); break; default: throw new InvalidOperationException(); } //load and init zone LoadAndInitZone(newZoneInfo, allRecords); //save zone file SaveZoneFile(newZoneInfo.Name); //post processing for catalog zones if (newType == AuthZoneType.Catalog) { //convert all member zones too CatalogZone newCatalogZone = newZoneInfo.ApexZone as CatalogZone; foreach (string memberZoneName in newCatalogZone.GetAllMemberZoneNames()) { AuthZoneInfo memberZoneInfo = GetAuthZoneInfo(memberZoneName); if (memberZoneInfo is null) continue; switch (memberZoneInfo.Type) { case AuthZoneType.Secondary: try { AuthZoneType originalMemberZoneType = newCatalogZone.GetZoneTypeProperty(memberZoneInfo.Name); if (originalMemberZoneType != AuthZoneType.Primary) { memberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name; //reset catalog zone object reference break; } AuthZoneInfo newMemberZoneInfo = ConvertZoneTypeTo(memberZoneInfo, AuthZoneType.Primary); newMemberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name; AuthZoneDnssecStatus dnssecStatus = memberZoneInfo.ApexZone.DnssecStatus; if (dnssecStatus != AuthZoneDnssecStatus.Unsigned) { //sign the new primary zone if the secondary zone was signed SecondaryZone secondaryZone = memberZoneInfo.ApexZone as SecondaryZone; IReadOnlyCollection dnssecPrivateKeys = secondaryZone.DnssecPrivateKeys; if (dnssecPrivateKeys is not null) { try { IReadOnlyList existingDnsKeyRecords = secondaryZone.GetRecords(DnsResourceRecordType.DNSKEY); uint dnsKeyTtl = existingDnsKeyRecords[0].OriginalTtlValue; bool useNSec3 = dnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3; ushort iterations = 0; byte[] salt = []; if (useNSec3) { IReadOnlyList existingNsec3ParamRecord = secondaryZone.GetRecords(DnsResourceRecordType.NSEC3PARAM); DnsNSEC3PARAMRecordData nsec3Param = existingNsec3ParamRecord[0].RDATA as DnsNSEC3PARAMRecordData; iterations = nsec3Param.Iterations; salt = nsec3Param.Salt; } PrimaryZone newPrimaryZone = newMemberZoneInfo.ApexZone as PrimaryZone; newPrimaryZone.SignZone(dnssecPrivateKeys, dnsKeyTtl, useNSec3, iterations, salt); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } } SaveZoneFile(newMemberZoneInfo.Name); } catch { //ignore errors since they were already logged } break; case AuthZoneType.SecondaryForwarder: try { AuthZoneInfo newMemberZoneInfo = ConvertZoneTypeTo(memberZoneInfo, AuthZoneType.Forwarder); newMemberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name; SaveZoneFile(newMemberZoneInfo.Name); } catch { //ignore errors since they were already logged } break; default: //stub zone memberZoneInfo.ApexZone.CatalogZoneName = newZoneInfo.Name; //reset catalog zone object reference break; } } } return newZoneInfo; } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server failed to convert the zone '" + currentZoneInfo.DisplayName + "' from " + currentZoneInfo.TypeName + " to " + AuthZoneInfo.GetZoneTypeName(newType) + " zone.\r\n" + ex.ToString()); //delete the zone if it was created DeleteZone(currentZoneInfo); //reload old zone file string zoneFile = Path.Combine(_dnsServer.ConfigFolder, "zones", currentZoneInfo.Name + ".zone"); _zoneIndexLock.EnterWriteLock(); try { using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) { AuthZoneInfo zoneInfo = LoadZoneFrom(fS, File.GetLastWriteTimeUtc(fS.SafeFileHandle)); _zoneIndex.Add(zoneInfo); _zoneIndex.Sort(); } _dnsServer.LogManager.Write("DNS Server successfully loaded zone file: " + zoneFile); } catch (Exception ex2) { _dnsServer.LogManager.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex2.ToString()); } finally { _zoneIndexLock.ExitWriteLock(); } throw; } } #endregion #region catalog member zones public void AddCatalogMemberZone(string catalogZoneName, AuthZoneInfo memberZoneInfo, bool ignoreValidationErrors = false) { switch (memberZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: if (!ignoreValidationErrors) { string currentCatalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName; if (currentCatalogZoneName is not null) throw new DnsServerException("The zone '" + memberZoneInfo.DisplayName + "' is already a member of Catalog zone '" + currentCatalogZoneName + "'."); } ApexZone apexZone = _root.GetApexZone(catalogZoneName); if (apexZone is not CatalogZone catalogZone) { if (ignoreValidationErrors) return; throw new DnsServerException("No such Catalog zone was found: " + catalogZoneName); } //set catalog zone name in member zone so that properties can be set below correctly memberZoneInfo.ApexZone.CatalogZoneName = catalogZone.Name; if (!memberZoneInfo.Disabled) { catalogZone.AddMemberZone(memberZoneInfo.Name, memberZoneInfo.Type); //update properties in catalog zone by settings member zone property values again switch (memberZoneInfo.Type) { case AuthZoneType.Primary: memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; memberZoneInfo.ZoneTransfer = memberZoneInfo.ZoneTransfer; memberZoneInfo.ZoneTransferTsigKeyNames = memberZoneInfo.ZoneTransferTsigKeyNames; break; case AuthZoneType.Secondary: memberZoneInfo.PrimaryNameServerAddresses = memberZoneInfo.PrimaryNameServerAddresses; memberZoneInfo.PrimaryZoneTransferProtocol = memberZoneInfo.PrimaryZoneTransferProtocol; memberZoneInfo.PrimaryZoneTransferTsigKeyName = memberZoneInfo.PrimaryZoneTransferTsigKeyName; memberZoneInfo.ValidateZone = memberZoneInfo.ValidateZone; memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; memberZoneInfo.ZoneTransfer = memberZoneInfo.ZoneTransfer; memberZoneInfo.ZoneTransferTsigKeyNames = memberZoneInfo.ZoneTransferTsigKeyNames; break; case AuthZoneType.Stub: memberZoneInfo.PrimaryNameServerAddresses = memberZoneInfo.PrimaryNameServerAddresses; memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; break; case AuthZoneType.Forwarder: memberZoneInfo.QueryAccess = memberZoneInfo.QueryAccess; break; } //save catalog changes SaveZoneFile(catalogZone.Name); } break; default: throw new NotSupportedException(); } } public void RemoveCatalogMemberZone(AuthZoneInfo memberZoneInfo, bool disableOnly = false) { switch (memberZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: case AuthZoneType.SecondaryForwarder: string catalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName; if (catalogZoneName is null) return; CatalogZone catalogZone = memberZoneInfo.ApexZone.CatalogZone; if (catalogZone is not null) { catalogZone.RemoveMemberZone(memberZoneInfo.Name); //save catalog changes SaveZoneFile(catalogZone.Name); } if (!disableOnly) memberZoneInfo.ApexZone.CatalogZoneName = null; break; default: throw new NotSupportedException(); } } public void ChangeCatalogMemberZoneOwnership(AuthZoneInfo memberZoneInfo, string newCatalogZoneName) { switch (memberZoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: string currentCatalogZoneName = memberZoneInfo.ApexZone.CatalogZoneName; if (currentCatalogZoneName is null) throw new DnsServerException("The zone '" + memberZoneInfo.DisplayName + "' is not a member of any Catalog zone."); AddCatalogMemberZone(newCatalogZoneName, memberZoneInfo, true); if (!memberZoneInfo.Disabled) { ApexZone apexZone = _root.GetApexZone(currentCatalogZoneName); if (apexZone is CatalogZone currentCatalogZone) { currentCatalogZone.ChangeMemberZoneOwnership(memberZoneInfo.Name, newCatalogZoneName); //save catalog changes SaveZoneFile(currentCatalogZone.Name); } } break; default: throw new NotSupportedException(); } } #endregion #region DNSSEC public void SignPrimaryZone(string zoneName, DnssecPrivateKey kskPrivateKey, DnssecPrivateKey zskPrivateKey, uint dnsKeyTtl, bool useNSec3, ushort iterations = 0, byte saltLength = 0) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.SignZone(kskPrivateKey, zskPrivateKey, dnsKeyTtl, useNSec3, iterations, saltLength); SaveZoneFile(primaryZone.Name); } public void UnsignPrimaryZone(string zoneName) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UnsignZone(); SaveZoneFile(primaryZone.Name); } public void ConvertPrimaryZoneToNSEC(string zoneName) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.ConvertToNSec(); SaveZoneFile(primaryZone.Name); } public void ConvertPrimaryZoneToNSEC3(string zoneName, ushort iterations, byte saltLength) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.ConvertToNSec3(iterations, saltLength); SaveZoneFile(primaryZone.Name); } public void UpdatePrimaryZoneNSEC3Parameters(string zoneName, ushort iterations, byte saltLength) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UpdateNSec3Parameters(iterations, saltLength); SaveZoneFile(primaryZone.Name); } public void UpdatePrimaryZoneDnsKeyTtl(string zoneName, uint dnsKeyTtl) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.UpdateDnsKeyTtl(dnsKeyTtl); SaveZoneFile(primaryZone.Name); } public DnssecPrivateKey GenerateAndAddPrimaryZoneDnssecPrivateKey(string zoneName, DnssecPrivateKeyType keyType, DnssecAlgorithm algorithm, ushort rolloverDays, int keySize = -1) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); DnssecPrivateKey privateKey = primaryZone.GenerateAndAddPrivateKey(keyType, algorithm, rolloverDays, keySize); SaveZoneFile(primaryZone.Name); return privateKey; } public void AddPrimaryZoneDnssecPrivateKey(string zoneName, DnssecPrivateKey privateKey) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.AddPrivateKey(privateKey); SaveZoneFile(primaryZone.Name); } public DnssecPrivateKey UpdatePrimaryZoneDnssecPrivateKey(string zoneName, ushort keyTag, ushort rolloverDays) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); DnssecPrivateKey privateKey = primaryZone.UpdatePrivateKey(keyTag, rolloverDays); SaveZoneFile(primaryZone.Name); return privateKey; } public void DeletePrimaryZoneDnssecPrivateKey(string zoneName, ushort keyTag) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.DeletePrivateKey(keyTag); SaveZoneFile(primaryZone.Name); } public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(string zoneName) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.PublishAllGeneratedKeys(); SaveZoneFile(primaryZone.Name); } public void RolloverPrimaryZoneDnsKey(string zoneName, ushort keyTag) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); primaryZone.RolloverDnsKey(keyTag); SaveZoneFile(primaryZone.Name); } public async Task RetirePrimaryZoneDnsKeyAsync(string zoneName, ushort keyTag) { if (!_root.TryGet(zoneName, out ApexZone apexZone) || (apexZone is not PrimaryZone primaryZone) || primaryZone.Internal) throw new DnsServerException("No such primary zone was found: " + zoneName); await primaryZone.RetireDnsKeyAsync(keyTag); SaveZoneFile(primaryZone.Name); } public void LoadTrustAnchorsTo(DnsClient dnsClient, string domain, DnsResourceRecordType type) { if (type == DnsResourceRecordType.DS) { domain = GetParentZone(domain); if (domain is null) domain = ""; } AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.FindAuthZoneInfo(domain, false); if ((zoneInfo is not null) && (zoneInfo.ApexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned)) { IReadOnlyList dnsKeyRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.DNSKEY); List dsRecords = new List(dnsKeyRecords.Count); foreach (DnsResourceRecord dnsKeyRecord in dnsKeyRecords) { DnsDNSKEYRecordData dnsKey = dnsKeyRecord.RDATA as DnsDNSKEYRecordData; if (dnsKey.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint) && !dnsKey.Flags.HasFlag(DnsDnsKeyFlag.Revoke)) dsRecords.Add(new DnsResourceRecord(dnsKeyRecord.Name, DnsResourceRecordType.DS, DnsClass.IN, 0, dnsKey.CreateDS(dnsKeyRecord.Name, DnssecDigestType.SHA256))); } //set trust anchor dnsClient.TrustAnchors[zoneInfo.Name] = dsRecords; } } #endregion #region zone listing public IEnumerable EnumerateAllZones() { _zoneIndexLock.EnterReadLock(); try { foreach (AuthZoneInfo zoneInfo in _zoneIndex) yield return zoneInfo; } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetAllZones() { _zoneIndexLock.EnterReadLock(); try { return _zoneIndex.ToArray(); } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetZones(Func predicate) { _zoneIndexLock.EnterReadLock(); try { List zoneInfoList = new List(); foreach (AuthZoneInfo zoneInfo in _zoneIndex) { if (predicate(zoneInfo)) zoneInfoList.Add(zoneInfo); } return zoneInfoList; } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetAllCatalogZones() { _zoneIndexLock.EnterReadLock(); try { return _catalogZoneIndex.ToArray(); } finally { _zoneIndexLock.ExitReadLock(); } } public IReadOnlyList GetCatalogZones(Func predicate) { _zoneIndexLock.EnterReadLock(); try { List catalogZoneInfoList = new List(); foreach (AuthZoneInfo zone in _catalogZoneIndex) { if (predicate(zone)) catalogZoneInfoList.Add(zone); } return catalogZoneInfoList; } finally { _zoneIndexLock.ExitReadLock(); } } #endregion #region zone record management public void ListAllZoneRecords(string zoneName, List records) { foreach (AuthZone authZone in _root.GetApexZoneWithSubDomainZones(zoneName)) authZone.ListAllRecords(records); } public void ListAllZoneRecords(string zoneName, DnsResourceRecordType[] types, List records) { foreach (AuthZone authZone in _root.GetApexZoneWithSubDomainZones(zoneName)) { foreach (DnsResourceRecordType type in types) records.AddRange(authZone.GetRecords(type)); } } public void ListAllRecords(string zoneName, string domain, List records) { ValidateIfDomainBelongsToZone(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) authZone.ListAllRecords(records); } public IEnumerable EnumerateAllRecords(string zoneName, string domain, bool includeAllSubDomainNames = false) { ValidateIfDomainBelongsToZone(zoneName, domain); if (includeAllSubDomainNames) { foreach (AuthZone authZone in _root.GetSubDomainZoneWithSubDomainZones(domain)) { foreach (KeyValuePair> entry in authZone.Entries) { foreach (DnsResourceRecord record in entry.Value) yield return record; } } } else { if (_root.TryGet(zoneName, domain, out AuthZone authZone)) { foreach (KeyValuePair> entry in authZone.Entries) { foreach (DnsResourceRecord record in entry.Value) yield return record; } } } } public IReadOnlyList GetRecords(string zoneName, string domain, DnsResourceRecordType type) { ValidateIfDomainBelongsToZone(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) return authZone.GetRecords(type); return Array.Empty(); } public IReadOnlyDictionary> GetEntriesFor(string zoneName, string domain) { ValidateIfDomainBelongsToZone(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) return authZone.Entries; return new Dictionary>(1); } public void SetRecords(string zoneName, IReadOnlyList records) { for (int i = 1; i < records.Count; i++) { if (!records[i].Name.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); if (records[i].Type != records[0].Type) throw new InvalidOperationException(); if (records[i].Class != records[0].Class) throw new InvalidOperationException(); } AuthZone authZone = GetOrAddSubDomainZone(zoneName, records[0].Name); authZone.SetRecords(records[0].Type, records); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } public void SetRecord(string zoneName, DnsResourceRecord record) { ValidateIfDomainBelongsToZone(zoneName, record.Name); AuthZone authZone = GetOrAddSubDomainZone(zoneName, record.Name); authZone.SetRecords(record.Type, new DnsResourceRecord[] { record }); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } public bool AddRecord(string zoneName, DnsResourceRecord record) { ValidateIfDomainBelongsToZone(zoneName, record.Name); AuthZone authZone = GetOrAddSubDomainZone(zoneName, record.Name); if (authZone.AddRecord(record)) { if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); return true; } return false; } public void UpdateRecord(string zoneName, DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { ValidateIfDomainBelongsToZone(zoneName, oldRecord.Name); ValidateIfDomainBelongsToZone(zoneName, newRecord.Name); if (oldRecord.Type != newRecord.Type) throw new DnsServerException("Cannot update record: new record must be of same type."); if (oldRecord.Type == DnsResourceRecordType.SOA) throw new DnsServerException("Cannot update record: use SetRecords() for updating SOA record."); if (!_root.TryGet(zoneName, oldRecord.Name, out AuthZone authZone)) throw new DnsServerException("Cannot update record: zone '" + zoneName + "' does not exists."); switch (oldRecord.Type) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: case DnsResourceRecordType.APP: if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) { authZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } else { authZone.DeleteRecords(oldRecord.Type); if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(oldRecord.Name, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } AuthZone newZone = GetOrAddSubDomainZone(zoneName, newRecord.Name); newZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); if (newZone is SubDomainZone subDomainZone1) subDomainZone1.AutoUpdateState(); } break; default: if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) { authZone.UpdateRecord(oldRecord, newRecord); if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } else { if (!authZone.DeleteRecord(oldRecord.Type, oldRecord.RDATA)) throw new DnsWebServiceException("Cannot update record: the old record does not exists."); if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(oldRecord.Name, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } AuthZone newZone = GetOrAddSubDomainZone(zoneName, newRecord.Name); newZone.AddRecord(newRecord); if (newZone is SubDomainZone subDomainZone1) subDomainZone1.AutoUpdateState(); } break; } } public bool DeleteRecord(string zoneName, DnsResourceRecord record) { return DeleteRecord(zoneName, record.Name, record.Type, record.RDATA); } public bool DeleteRecord(string zoneName, string domain, DnsResourceRecordType type, DnsResourceRecordData rdata) { ValidateIfDomainBelongsToZone(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) { if (authZone.DeleteRecord(type, rdata)) { if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(domain, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } return true; } } return false; } public bool DeleteRecords(string zoneName, string domain, DnsResourceRecordType type) { ValidateIfDomainBelongsToZone(zoneName, domain); if (_root.TryGet(zoneName, domain, out AuthZone authZone)) { if (authZone.DeleteRecords(type)) { if (authZone is SubDomainZone subDomainZone) { if (authZone.IsEmpty) _root.TryRemove(domain, out SubDomainZone _); //remove empty sub zone else subDomainZone.AutoUpdateState(); } return true; } } return false; } #endregion #region zone transfer / import public IReadOnlyList QueryZoneTransferRecords(string zoneName) { AuthZoneInfo zoneInfo = GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new InvalidOperationException("Zone was not found: " + zoneName); //primary, secondary, and forwarder zones support zone transfer IReadOnlyList soaRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count != 1) throw new InvalidOperationException("Zone must be a primary, secondary, or forwarder zone."); DnsResourceRecord soaRecord = soaRecords[0]; List records = new List(); ListAllZoneRecords(zoneName, records); List xfrRecords = new List(records.Count + 1); //start message xfrRecords.Add(soaRecord); foreach (DnsResourceRecord record in records) { GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo(); if (authRecordInfo.Disabled) continue; switch (record.Type) { case DnsResourceRecordType.SOA: break; //skip record case DnsResourceRecordType.NS: xfrRecords.Add(record); IReadOnlyList glueRecords = (authRecordInfo as NSRecordInfo).GlueRecords; if (glueRecords is not null) { foreach (DnsResourceRecord glueRecord in glueRecords) xfrRecords.Add(glueRecord); } break; default: xfrRecords.Add(record); break; } } //end message xfrRecords.Add(soaRecord); return xfrRecords; } public IReadOnlyList QueryIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord clientSoaRecord) { AuthZoneInfo authZone = GetAuthZoneInfo(zoneName, true); if (authZone is null) throw new InvalidOperationException("Zone was not found: " + zoneName); //primary, secondary, forwarder, and catalog zones support zone transfer IReadOnlyList soaRecords = authZone.ApexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count != 1) throw new InvalidOperationException("No SOA record was found for IXFR."); DnsResourceRecord currentSoaRecord = soaRecords[0]; uint clientSerial = (clientSoaRecord.RDATA as DnsSOARecordData).Serial; if (clientSerial == (currentSoaRecord.RDATA as DnsSOARecordData).Serial) { //zone not modified return [currentSoaRecord]; } //find history record start from client serial IReadOnlyList zoneHistory = authZone.ZoneHistory; int index = 0; while (index < zoneHistory.Count) { //check difference sequence if ((zoneHistory[index].RDATA as DnsSOARecordData).Serial == clientSerial) break; //found history for client's serial //skip to next difference sequence index++; int soaCount = 1; while (index < zoneHistory.Count) { if (zoneHistory[index].Type == DnsResourceRecordType.SOA) { soaCount++; if (soaCount == 3) break; } index++; } } if (index == zoneHistory.Count) { //client's serial was not found in zone history //do full zone transfer return QueryZoneTransferRecords(zoneName); } List xfrRecords = new List(); //start incremental message xfrRecords.Add(currentSoaRecord); //write history for (int i = index; i < zoneHistory.Count; i++) xfrRecords.Add(zoneHistory[i]); //end incremental message xfrRecords.Add(currentSoaRecord); //condense return CondenseIncrementalZoneTransferRecords(zoneName, clientSoaRecord, xfrRecords); } public void SyncZoneTransferRecords(string zoneName, IReadOnlyList xfrRecords) { if ((xfrRecords.Count < 2) || (xfrRecords[0].Type != DnsResourceRecordType.SOA) || !xfrRecords[0].Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || !xfrRecords[xfrRecords.Count - 1].Equals(xfrRecords[0])) throw new DnsServerException("Invalid AXFR response was received."); List latestRecords = new List(xfrRecords.Count); List allGlueRecords = new List(4); if (zoneName.Length == 0) { //root zone case for (int i = 1; i < xfrRecords.Count; i++) { DnsResourceRecord record = xfrRecords[i]; switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!allGlueRecords.Contains(record)) allGlueRecords.Add(record); break; default: if (!latestRecords.Contains(record)) latestRecords.Add(record); break; } } } else { for (int i = 1; i < xfrRecords.Count; i++) { DnsResourceRecord record = xfrRecords[i]; if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { if (!latestRecords.Contains(record)) latestRecords.Add(record); } else if (!allGlueRecords.Contains(record)) { allGlueRecords.Add(record); } } } if (allGlueRecords.Count > 0) { foreach (DnsResourceRecord record in latestRecords) { if (record.Type == DnsResourceRecordType.NS) record.SyncGlueRecords(allGlueRecords); } } //sync records List currentRecords = new List(); ListAllZoneRecords(zoneName, currentRecords); Dictionary>> currentRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(currentRecords); Dictionary>> latestRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(latestRecords); //remove domains that do not exists in new records foreach (KeyValuePair>> currentDomain in currentRecordsGroupedByDomain) { if (!latestRecordsGroupedByDomain.ContainsKey(currentDomain.Key)) _root.TryRemove(currentDomain.Key, out SubDomainZone _); } //sync new records foreach (KeyValuePair>> latestEntries in latestRecordsGroupedByDomain) { AuthZone zone = GetOrAddSubDomainZone(zoneName, latestEntries.Key); if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(latestEntries.Value); else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(latestEntries.Value); } if (!_root.TryGet(zoneName, out ApexZone apexZone)) throw new InvalidOperationException(); apexZone.UpdateDnssecStatus(); SaveZoneFile(apexZone.Name); } public IReadOnlyList SyncIncrementalZoneTransferRecords(string zoneName, IReadOnlyList xfrRecords) { if ((xfrRecords.Count < 2) || (xfrRecords[0].Type != DnsResourceRecordType.SOA) || !xfrRecords[0].Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || !xfrRecords[xfrRecords.Count - 1].Equals(xfrRecords[0])) throw new DnsServerException("Invalid IXFR/AXFR response was received."); if ((xfrRecords.Count < 4) || (xfrRecords[1].Type != DnsResourceRecordType.SOA)) { //received AXFR response SyncZoneTransferRecords(zoneName, xfrRecords); return Array.Empty(); } if (!_root.TryGet(zoneName, out ApexZone apexZone)) throw new InvalidOperationException("No such zone was found: " + zoneName); IReadOnlyList soaRecords = apexZone.GetRecords(DnsResourceRecordType.SOA); if (soaRecords.Count != 1) throw new InvalidOperationException("No authoritative zone was found: " + zoneName); //process IXFR response DnsResourceRecord currentSoaRecord = soaRecords[0]; DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData; List condensedXfrRecords = CondenseIncrementalZoneTransferRecords(zoneName, currentSoaRecord, xfrRecords); List deletedRecords = new List(); List deletedGlueRecords = new List(); List addedRecords = new List(); List addedGlueRecords = new List(); //read and apply difference sequences int index = 1; int count = condensedXfrRecords.Count - 1; while (index < count) { //read deleted records DnsResourceRecord deletedSoaRecord = condensedXfrRecords[index]; if ((deletedSoaRecord.Type != DnsResourceRecordType.SOA) || !deletedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); index++; while (index < count) { DnsResourceRecord record = condensedXfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: deletedGlueRecords.Add(record); break; default: deletedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { deletedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: deletedGlueRecords.Add(record); break; } } } index++; } //read added records DnsResourceRecord addedSoaRecord = condensedXfrRecords[index]; if (!addedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); index++; while (index < count) { DnsResourceRecord record = condensedXfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: addedGlueRecords.Add(record); break; default: addedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { addedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: addedGlueRecords.Add(record); break; } } } index++; } //check sequence soa serial DnsSOARecordData deletedSoa = deletedSoaRecord.RDATA as DnsSOARecordData; if (currentSoa.Serial != deletedSoa.Serial) throw new InvalidOperationException("Current SOA serial does not match with the IXFR difference sequence deleted SOA."); //sync difference sequence if (deletedRecords.Count > 0) { foreach (KeyValuePair>> deletedEntry in DnsResourceRecord.GroupRecords(deletedRecords)) { AuthZone zone = GetOrAddSubDomainZone(zoneName, deletedEntry.Key); if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { zone.SyncRecords(deletedEntry.Value, null); } else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { zone.SyncRecords(deletedEntry.Value, null); if (zone.IsEmpty) _root.TryRemove(deletedEntry.Key, out SubDomainZone _); //remove empty sub zone } } } if (addedRecords.Count > 0) { foreach (KeyValuePair>> addedEntry in DnsResourceRecord.GroupRecords(addedRecords)) { AuthZone zone = GetOrAddSubDomainZone(zoneName, addedEntry.Key); if (zone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(null, addedEntry.Value); else if ((zone is SubDomainZone subDomainZone) && subDomainZone.AuthoritativeZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) zone.SyncRecords(null, addedEntry.Value); } } if ((deletedGlueRecords.Count > 0) || (addedGlueRecords.Count > 0)) { foreach (AuthZone zone in _root.GetApexZoneWithSubDomainZones(zoneName)) zone.SyncGlueRecords(deletedGlueRecords, addedGlueRecords); } { AuthZone zone = GetOrAddSubDomainZone(zoneName, zoneName); addedSoaRecord.CopyRecordInfoFrom(currentSoaRecord); zone.LoadRecords(DnsResourceRecordType.SOA, new DnsResourceRecord[] { addedSoaRecord }); } //check next difference sequence currentSoa = addedSoaRecord.RDATA as DnsSOARecordData; deletedRecords.Clear(); deletedGlueRecords.Clear(); addedRecords.Clear(); addedGlueRecords.Clear(); } apexZone.UpdateDnssecStatus(); SaveZoneFile(apexZone.Name); //return history List historyRecords = new List(xfrRecords.Count - 2); for (int i = 1; i < xfrRecords.Count - 1; i++) historyRecords.Add(xfrRecords[i]); return historyRecords; } private static List CondenseIncrementalZoneTransferRecords(string zoneName, DnsResourceRecord currentSoaRecord, IReadOnlyList xfrRecords) { DnsResourceRecord firstSoaRecord = xfrRecords[0]; DnsResourceRecord lastSoaRecord = xfrRecords[xfrRecords.Count - 1]; DnsResourceRecord firstDeletedSoaRecord = null; DnsResourceRecord lastAddedSoaRecord = null; List deletedRecords = new List(); List deletedGlueRecords = new List(); List addedRecords = new List(); List addedGlueRecords = new List(); //read and apply difference sequences int index = 1; int count = xfrRecords.Count - 1; DnsSOARecordData currentSoa = (DnsSOARecordData)currentSoaRecord.RDATA; while (index < count) { //read deleted records DnsResourceRecord deletedSoaRecord = xfrRecords[index]; if ((deletedSoaRecord.Type != DnsResourceRecordType.SOA) || !deletedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); if (firstDeletedSoaRecord is null) firstDeletedSoaRecord = deletedSoaRecord; index++; while (index < count) { DnsResourceRecord record = xfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!addedGlueRecords.Remove(record)) deletedGlueRecords.Add(record); break; default: if (!addedRecords.Remove(record)) deletedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { if (!addedRecords.Remove(record)) deletedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!addedGlueRecords.Remove(record)) deletedGlueRecords.Add(record); break; } } } index++; } //read added records DnsResourceRecord addedSoaRecord = xfrRecords[index]; if (!addedSoaRecord.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException(); lastAddedSoaRecord = addedSoaRecord; index++; while (index < count) { DnsResourceRecord record = xfrRecords[index]; if (record.Type == DnsResourceRecordType.SOA) break; if (zoneName.Length == 0) { //root zone case switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!deletedGlueRecords.Remove(record)) addedGlueRecords.Add(record); break; default: if (!deletedRecords.Remove(record)) addedRecords.Add(record); break; } } else { if (record.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneName, StringComparison.OrdinalIgnoreCase)) { if (!deletedRecords.Remove(record)) addedRecords.Add(record); } else { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (!deletedGlueRecords.Remove(record)) addedGlueRecords.Add(record); break; } } } index++; } //check sequence soa serial DnsSOARecordData deletedSoa = deletedSoaRecord.RDATA as DnsSOARecordData; if (currentSoa.Serial != deletedSoa.Serial) throw new InvalidOperationException("Current SOA serial does not match with the IXFR difference sequence deleted SOA."); //check next difference sequence currentSoa = addedSoaRecord.RDATA as DnsSOARecordData; } //create condensed records List condensedRecords = new List(2 + 2 + deletedRecords.Count + deletedGlueRecords.Count + addedRecords.Count + addedGlueRecords.Count); condensedRecords.Add(firstSoaRecord); condensedRecords.Add(firstDeletedSoaRecord); condensedRecords.AddRange(deletedRecords); condensedRecords.AddRange(deletedGlueRecords); condensedRecords.Add(lastAddedSoaRecord); condensedRecords.AddRange(addedRecords); condensedRecords.AddRange(addedGlueRecords); condensedRecords.Add(lastSoaRecord); return condensedRecords; } internal void ImportRecords(string zoneName, IReadOnlyList records, bool overwrite, bool overwriteSoaSerial) { _ = _root.FindZone(zoneName, out _, out _, out ApexZone apexZone, out _); if ((apexZone is null) || !apexZone.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) throw new DnsServerException("No such zone was found: " + zoneName); if ((apexZone is not PrimaryZone) && (apexZone is not ForwarderZone)) throw new DnsServerException("Zone must be a primary or forwarder type: " + apexZone.ToString()); List soaRRSet = null; foreach (KeyValuePair>> zoneEntry in DnsResourceRecord.GroupRecords(records)) { if (zoneName.Equals(zoneEntry.Key, StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { switch (rrsetEntry.Key) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); break; case DnsResourceRecordType.SOA: if (!overwriteSoaSerial) rrsetEntry.Value[0].GetAuthSOARecordInfo().UseSoaSerialDateScheme = apexZone.GetRecords(DnsResourceRecordType.SOA)[0].GetAuthSOARecordInfo().UseSoaSerialDateScheme; apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); soaRRSet = rrsetEntry.Value; break; default: if (overwrite) { apexZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); } else { foreach (DnsResourceRecord record in rrsetEntry.Value) apexZone.AddRecord(record); } break; } } } else { ValidateIfDomainBelongsToZone(zoneName, zoneEntry.Key); AuthZone authZone = GetOrAddSubDomainZone(zoneName, zoneEntry.Key); foreach (KeyValuePair> rrsetEntry in zoneEntry.Value) { switch (rrsetEntry.Key) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: case DnsResourceRecordType.SOA: authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); break; default: if (overwrite) { authZone.SetRecords(rrsetEntry.Key, rrsetEntry.Value); } else { foreach (DnsResourceRecord record in rrsetEntry.Value) authZone.AddRecord(record); } break; } } if (authZone is SubDomainZone subDomainZone) subDomainZone.AutoUpdateState(); } } if (overwriteSoaSerial && (soaRRSet is not null) && ((apexZone is PrimaryZone) || (apexZone is ForwarderZone))) apexZone.SetSoaSerial((soaRRSet[0].RDATA as DnsSOARecordData).Serial); SaveZoneFile(apexZone.Name); } #endregion #region query processing public DnsDatagram QueryClosestDelegation(DnsDatagram request) { _ = _root.FindZone(request.Question[0].Name, out _, out SubDomainZone delegation, out ApexZone apexZone, out _); if (delegation is not null) { bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned); return GetReferralResponse(request, dnssecOk, delegation, apexZone); } if (apexZone is StubZone) return GetReferralResponse(request, false, apexZone, apexZone); //no delegation found return null; } public async Task QueryAsync(DnsDatagram request, IPAddress remoteIP, bool isRecursionAllowed) { AuthZone zone = _root.FindZone(request.Question[0].Name, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone apexZone, out bool hasSubDomains); if ((apexZone is null) || !apexZone.IsActive) return null; //no authority for requested zone if (!await IsQueryAllowedAsync(apexZone, remoteIP)) return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.Refused, request.Question); return InternalQuery(request, isRecursionAllowed, zone, closest, delegation, apexZone, hasSubDomains); } public DnsDatagram Query(DnsDatagram request, bool isRecursionAllowed) { AuthZone zone = _root.FindZone(request.Question[0].Name, out SubDomainZone closest, out SubDomainZone delegation, out ApexZone apexZone, out bool hasSubDomains); if ((apexZone is null) || !apexZone.IsActive) return null; //no authority for requested zone return InternalQuery(request, isRecursionAllowed, zone, closest, delegation, apexZone, hasSubDomains); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private DnsDatagram InternalQuery(DnsDatagram request, bool isRecursionAllowed, AuthZone zone, SubDomainZone closest, SubDomainZone delegation, ApexZone apexZone, bool hasSubDomains) { DnsQuestionRecord question = request.Question[0]; bool dnssecOk = request.DnssecOk && (apexZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned); if ((zone is null) || !zone.IsActive) { //zone not found if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) return GetReferralResponse(request, dnssecOk, delegation, apexZone); if (apexZone is StubZone) return GetReferralResponse(request, false, apexZone, apexZone); DnsResponseCode rCode = DnsResponseCode.NoError; IReadOnlyList answer = null; IReadOnlyList authority = null; if (closest is not null) { answer = closest.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } else { answer = null; authority = closest.QueryRecords(DnsResourceRecordType.APP, false); } } if (((answer is null) || (answer.Count == 0)) && ((authority is null) || (authority.Count == 0))) { answer = apexZone.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } else { answer = null; authority = apexZone.QueryRecords(DnsResourceRecordType.APP, false); if (authority.Count == 0) { if ((apexZone is ForwarderZone) || (apexZone is SecondaryForwarderZone)) return GetForwarderResponse(request, null, closest, apexZone); //no DNAME or APP record available so process FWD response if (!hasSubDomains) rCode = DnsResponseCode.NxDomain; authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk); if (dnssecOk) { //add proof of non existence (NXDOMAIN) to prove the qname does not exists IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNxDomain(question.Name, false); else nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, false); if (nsecRecords.Count > 0) { List newAuthority = new List(authority.Count + nsecRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(nsecRecords); authority = newAuthority; } } } } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority); } else { //zone found if (question.Type == DnsResourceRecordType.DS) { if (zone is ApexZone) { if ((delegation is null) || !delegation.IsActive || !delegation.AuthoritativeZone.IsActive || (delegation.Name.Length > apexZone.Name.Length)) return null; //no authoritative parent side delegation zone available to answer for DS zone = delegation; //switch zone to parent side sub domain delegation zone for DS record if (request.DnssecOk) dnssecOk = delegation.AuthoritativeZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned; } } else if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) { //zone is delegation return GetReferralResponse(request, dnssecOk, delegation, apexZone); } DnsResponseCode rCode = DnsResponseCode.NoError; IReadOnlyList answer = null; IReadOnlyList authority = null; IReadOnlyList additional = null; if (closest is not null) { answer = closest.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } } if (((answer is null) || (answer.Count == 0)) && (question.Name.Length > apexZone.Name.Length)) { //query for DNAME only for subdomain names answer = apexZone.QueryRecords(DnsResourceRecordType.DNAME, dnssecOk); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { if (!DoDNAMESubstitution(question, dnssecOk, answer, out answer)) rCode = DnsResponseCode.YXDomain; } } if ((answer is null) || (answer.Count == 0)) { answer = zone.QueryRecords(question.Type, dnssecOk); if (answer.Count == 0) { //record type not found if (question.Type == DnsResourceRecordType.DS) { //check for correct auth zone if (apexZone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase)) { //current auth zone is child side; find parent side auth zone for DS string parentZone = GetParentZone(question.Name); if (parentZone is null) parentZone = string.Empty; _ = _root.FindZone(parentZone, out _, out _, out apexZone, out _); if ((apexZone is null) || !apexZone.IsActive) return null; //no authority for requested zone } } else { //check for delegation, stub & forwarder if ((delegation is not null) && delegation.IsActive && (delegation.Name.Length > apexZone.Name.Length)) return GetReferralResponse(request, dnssecOk, delegation, apexZone); if (apexZone is StubZone) return GetReferralResponse(request, false, apexZone, apexZone); } authority = zone.QueryRecords(DnsResourceRecordType.APP, false); if (authority.Count == 0) { if ((apexZone is ForwarderZone) || (apexZone is SecondaryForwarderZone)) return GetForwarderResponse(request, zone, closest, apexZone); //no APP record available so process FWD response authority = apexZone.QueryRecords(DnsResourceRecordType.SOA, dnssecOk); if (dnssecOk) { //add proof of non existence (NODATA) to prove that no such type or record exists IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNoData(question.Name, zone, apexZone); else nsecRecords = _root.FindNSecProofOfNonExistenceNoData(question.Name, zone); if (nsecRecords.Count > 0) { List newAuthority = new List(authority.Count + nsecRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(nsecRecords); authority = newAuthority; } } } additional = null; } else { //record type found if (zone.Name.StartsWith('*') && !zone.Name.Equals(question.Name, StringComparison.OrdinalIgnoreCase)) { //wildcard zone; generate new answer records DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answer.Count]; for (int i = 0; i < answer.Count; i++) wildcardAnswers[i] = new DnsResourceRecord(question.Name, answer[i].Type, answer[i].Class, answer[i].TTL, answer[i].RDATA) { Tag = answer[i].Tag }; answer = wildcardAnswers; //add proof of non existence (WILDCARD) to prove that the wildcard expansion was legit and the qname actually does not exists if (dnssecOk) { IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNxDomain(question.Name, true); else nsecRecords = _root.FindNSecProofOfNonExistenceNxDomain(question.Name, true); if (nsecRecords.Count > 0) authority = nsecRecords; } } DnsResourceRecord lastRR = answer[answer.Count - 1]; if ((lastRR.Type != question.Type) && (question.Type != DnsResourceRecordType.ANY)) { switch (lastRR.Type) { case DnsResourceRecordType.CNAME: List newAnswers = new List(answer.Count + 1); newAnswers.AddRange(answer); ResolveCNAME(question, dnssecOk, lastRR, newAnswers); answer = newAnswers; break; case DnsResourceRecordType.ANAME: case DnsResourceRecordType.ALIAS: authority = apexZone.GetRecords(DnsResourceRecordType.SOA); //adding SOA for use with NO DATA response break; } } switch (question.Type) { case DnsResourceRecordType.NS: case DnsResourceRecordType.MX: case DnsResourceRecordType.SRV: case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: additional = GetAdditionalRecords(answer, dnssecOk); break; default: additional = null; break; } } } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, isRecursionAllowed, false, false, rCode, request.Question, answer, authority, additional); } } private static async Task IsQueryAllowedAsync(ApexZone apexZone, IPAddress remoteIP) { async Task IsZoneNameServerAllowedAsync() { IReadOnlyList zoneNameServers = await apexZone.GetAllResolvedNameServerAddressesAsync(); foreach (NameServerAddress nameServer in zoneNameServers) { if (nameServer.IPEndPoint.Address.Equals(remoteIP)) return true; } return false; } CatalogZone catalogZone = apexZone.CatalogZone; if (catalogZone is not null) { if (!apexZone.OverrideCatalogQueryAccess) apexZone = catalogZone; //use catalog query access options } else { SecondaryCatalogZone secondaryCatalogZone = apexZone.SecondaryCatalogZone; if (secondaryCatalogZone is not null) { if (!apexZone.OverrideCatalogQueryAccess) apexZone = secondaryCatalogZone; //use secondary query access options } } switch (apexZone.QueryAccess) { case AuthZoneQueryAccess.Allow: return true; case AuthZoneQueryAccess.AllowOnlyPrivateNetworks: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; switch (remoteIP.AddressFamily) { case AddressFamily.InterNetwork: case AddressFamily.InterNetworkV6: return NetUtilities.IsPrivateIP(remoteIP); default: return false; } case AuthZoneQueryAccess.AllowOnlyZoneNameServers: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return await IsZoneNameServerAllowedAsync(); case AuthZoneQueryAccess.UseSpecifiedNetworkACL: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return NetworkAccessControl.IsAddressAllowed(remoteIP, apexZone.QueryAccessNetworkACL); case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return NetworkAccessControl.IsAddressAllowed(remoteIP, apexZone.QueryAccessNetworkACL) || await IsZoneNameServerAllowedAsync(); default: if (IPAddress.IsLoopback(remoteIP) || IPAddress.Any.Equals(remoteIP)) return true; return false; } } private void ResolveCNAME(DnsQuestionRecord question, bool dnssecOk, DnsResourceRecord lastCNAME, List answerRecords) { int queryCount = 0; do { string cnameDomain = (lastCNAME.RDATA as DnsCNAMERecordData).Domain; if (lastCNAME.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase)) break; //loop detected if (!_root.TryGet(cnameDomain, out AuthZoneNode zoneNode)) break; IReadOnlyList records = zoneNode.QueryRecords(question.Type, dnssecOk); if (records.Count < 1) break; DnsResourceRecord lastRR = records[records.Count - 1]; if (lastRR.Type != DnsResourceRecordType.CNAME) { answerRecords.AddRange(records); break; } foreach (DnsResourceRecord answerRecord in answerRecords) { if (answerRecord.Type != DnsResourceRecordType.CNAME) continue; if (answerRecord.RDATA.Equals(lastRR.RDATA)) return; //loop detected } answerRecords.AddRange(records); lastCNAME = lastRR; } while (++queryCount < DnsServer.MAX_CNAME_HOPS); } private bool DoDNAMESubstitution(DnsQuestionRecord question, bool dnssecOk, IReadOnlyList answer, out IReadOnlyList newAnswer) { DnsResourceRecord dnameRR = answer[0]; string result = (dnameRR.RDATA as DnsDNAMERecordData).Substitute(question.Name, dnameRR.Name); if (DnsClient.IsDomainNameValid(result)) { DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result)); List list = new List(5); list.AddRange(answer); list.Add(cnameRR); ResolveCNAME(question, dnssecOk, cnameRR, list); newAnswer = list; return true; } else { newAnswer = answer; return false; } } private List GetAdditionalRecords(IReadOnlyList refRecords, bool dnssecOk) { List additionalRecords = new List(refRecords.Count); foreach (DnsResourceRecord refRecord in refRecords) { switch (refRecord.Type) { case DnsResourceRecordType.NS: IReadOnlyList glueRecords = refRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) { additionalRecords.AddRange(glueRecords); } else { ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsNSRecordData).NameServer, dnssecOk, additionalRecords); } break; case DnsResourceRecordType.MX: ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsMXRecordData).Exchange, dnssecOk, additionalRecords); break; case DnsResourceRecordType.SRV: ResolveAdditionalRecords(refRecord, (refRecord.RDATA as DnsSRVRecordData).Target, dnssecOk, additionalRecords); break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: DnsSVCBRecordData svcb = refRecord.RDATA as DnsSVCBRecordData; string targetName = svcb.TargetName; if (svcb.SvcPriority == 0) { //For AliasMode SVCB RRs, a TargetName of "." indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12] if ((targetName.Length == 0) || targetName.Equals(refRecord.Name, StringComparison.OrdinalIgnoreCase)) break; } else { //For ServiceMode SVCB RRs, if TargetName has the value ".", then the owner name of this record MUST be used as the effective TargetName [draft-ietf-dnsop-svcb-https-12] if (targetName.Length == 0) targetName = refRecord.Name; } ResolveAdditionalRecords(refRecord, targetName, dnssecOk, additionalRecords); break; } } return additionalRecords; } private void ResolveAdditionalRecords(DnsResourceRecord refRecord, string domain, bool dnssecOk, List additionalRecords) { int count = 0; while (count++ < DnsServer.MAX_CNAME_HOPS) { AuthZone zone = _root.FindZone(domain, out _, out _, out _, out _); if ((zone is null) || !zone.IsActive) break; if (((refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) && ((refRecord.RDATA as DnsSVCBRecordData).SvcPriority == 0)) { //resolve SVCB/HTTPS for Alias mode refRecord IReadOnlyList records = zone.QueryRecordsWildcard(refRecord.Type, dnssecOk, domain); if ((records.Count > 0) && (records[0].Type == refRecord.Type) && (records[0].RDATA is DnsSVCBRecordData svcb)) { additionalRecords.AddRange(records); string targetName = svcb.TargetName; if (svcb.SvcPriority == 0) { //Alias mode if ((targetName.Length == 0) || targetName.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase)) break; //For AliasMode SVCB RRs, a TargetName of "." indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12] foreach (DnsResourceRecord additionalRecord in additionalRecords) { if (additionalRecord.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase)) return; //loop detected } //continue to resolve SVCB/HTTPS further domain = targetName; refRecord = records[0]; continue; } else { //Service mode if (targetName.Length > 0) { //continue to resolve A/AAAA for target name domain = targetName; refRecord = records[0]; continue; } //resolve A/AAAA below } } } bool hasA = false; bool hasAAAA = false; if ((refRecord.Type == DnsResourceRecordType.SRV) || (refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) { foreach (DnsResourceRecord additionalRecord in additionalRecords) { if (additionalRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) { switch (additionalRecord.Type) { case DnsResourceRecordType.A: hasA = true; break; case DnsResourceRecordType.AAAA: hasAAAA = true; break; } } if (hasA && hasAAAA) break; } } if (!hasA) { IReadOnlyList records = zone.QueryRecordsWildcard(DnsResourceRecordType.A, dnssecOk, domain); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.A)) additionalRecords.AddRange(records); } if (!hasAAAA) { IReadOnlyList records = zone.QueryRecordsWildcard(DnsResourceRecordType.AAAA, dnssecOk, domain); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.AAAA)) additionalRecords.AddRange(records); } break; } } private DnsDatagram GetReferralResponse(DnsDatagram request, bool dnssecOk, AuthZone delegationZone, ApexZone apexZone) { IReadOnlyList authority; if (delegationZone is StubZone) { authority = delegationZone.GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant query //update last used on DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in authority) record.GetAuthGenericRecordInfo().LastUsedOn = utcNow; } else { authority = delegationZone.QueryRecords(DnsResourceRecordType.NS, false); if (dnssecOk) { IReadOnlyList dsRecords = delegationZone.QueryRecords(DnsResourceRecordType.DS, true); if (dsRecords.Count > 0) { List newAuthority = new List(authority.Count + dsRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(dsRecords); authority = newAuthority; } else { //add proof of non existence (NODATA) to prove DS record does not exists IReadOnlyList nsecRecords; if (apexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) nsecRecords = _root.FindNSec3ProofOfNonExistenceNoData(request.Question[0].Name, delegationZone, apexZone); else nsecRecords = _root.FindNSecProofOfNonExistenceNoData(request.Question[0].Name, delegationZone); if (nsecRecords.Count > 0) { List newAuthority = new List(authority.Count + nsecRecords.Count); newAuthority.AddRange(authority); newAuthority.AddRange(nsecRecords); authority = newAuthority; } } } } IReadOnlyList additional = GetAdditionalRecords(authority, dnssecOk); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional); } private DnsDatagram GetForwarderResponse(DnsDatagram request, AuthZone zone, SubDomainZone closestZone, ApexZone forwarderZone) { IReadOnlyList authority = null; if (zone is not null) { if (zone.ContainsNameServerRecords()) return GetReferralResponse(request, false, zone, forwarderZone); authority = zone.QueryRecords(DnsResourceRecordType.FWD, false); } if (((authority is null) || (authority.Count == 0)) && (closestZone is not null)) { if (closestZone.ContainsNameServerRecords()) return GetReferralResponse(request, false, closestZone, forwarderZone); authority = closestZone.QueryRecords(DnsResourceRecordType.FWD, false); } if ((authority is null) || (authority.Count == 0)) { if (forwarderZone.ContainsNameServerRecords()) return GetReferralResponse(request, false, forwarderZone, forwarderZone); authority = forwarderZone.QueryRecords(DnsResourceRecordType.FWD, false); } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority); } #endregion #region properties public uint DefaultRecordTtl { get { return _defaultRecordTtl; } set { _defaultRecordTtl = value; } } public uint DefaultNsRecordTtl { get { return _defaultNsRecordTtl; } set { _defaultNsRecordTtl = value; } } public uint DefaultSoaRecordTtl { get { return _defaultSoaRecordTtl; } set { _defaultSoaRecordTtl = value; } } public bool UseSoaSerialDateScheme { get { return _useSoaSerialDateScheme; } set { _useSoaSerialDateScheme = value; } } public uint MinSoaRefresh { get { return _minSoaRefresh; } set { _minSoaRefresh = value; } } public uint MinSoaRetry { get { return _minSoaRetry; } set { _minSoaRetry = value; } } public int TotalZones { get { return _zoneIndex.Count; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ZoneManagers/BlockListZoneManager.cs ================================================ /* 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 System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Http.Client; namespace DnsServerCore.Dns.ZoneManagers { public sealed class BlockListZoneManager : IDisposable { #region variables readonly static char[] _popWordSeperator = new char[] { ' ', '\t' }; readonly static char[] _trimSeperator = new char[] { ' ', '\t', '*', '.' }; readonly DnsServer _dnsServer; readonly string _localCacheFolder; IReadOnlyList _blockListUrls = []; Dictionary _allowListZone = new Dictionary(); Dictionary> _blockListZone = new Dictionary>(); DnsSOARecordData _soaRecord; DnsNSRecordData _nsRecord; readonly IReadOnlyCollection _aRecords = [new DnsARecordData(IPAddress.Any)]; readonly IReadOnlyCollection _aaaaRecords = [new DnsAAAARecordData(IPAddress.IPv6Any)]; Timer _blockListUpdateTimer; DateTime _blockListLastUpdatedOn; int _blockListUpdateIntervalHours = 24; const int BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL = 5000; const int BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL = 900000; Timer _temporaryDisableBlockingTimer; DateTime _temporaryDisableBlockingTill; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor public BlockListZoneManager(DnsServer dnsServer) { _dnsServer = dnsServer; _localCacheFolder = Path.Combine(_dnsServer.ConfigFolder, "blocklists"); if (!Directory.Exists(_localCacheFolder)) Directory.CreateDirectory(_localCacheFolder); UpdateServerDomain(); _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { if (_pendingSave) { try { SaveConfigFileInternal(); _pendingSave = false; } catch (Exception ex) { _dnsServer.LogManager.Write(ex); //set timer to retry again _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _blockListUpdateTimer?.Dispose(); _temporaryDisableBlockingTimer?.Dispose(); lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveConfigFileInternal(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { _pendingSave = false; } } } _disposed = true; } #endregion #region config public void LoadConfigFile() { string blockListConfigFile = Path.Combine(_dnsServer.ConfigFolder, "blocklist.config"); try { using (FileStream fS = new FileStream(blockListConfigFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS, false); } _dnsServer.LogManager.Write("DNS Server block list config file was loaded: " + blockListConfigFile); } catch (FileNotFoundException) { SaveConfigFileInternal(); } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server encountered an error while loading block list config file: " + blockListConfigFile + "\r\n" + ex.ToString()); } } public void LoadConfig(Stream s, bool isConfigTransfer) { lock (_saveLock) { ReadConfigFrom(s, isConfigTransfer); SaveConfigFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void SaveConfigFileInternal() { string blockListConfigFile = Path.Combine(_dnsServer.ConfigFolder, "blocklist.config"); using (MemoryStream mS = new MemoryStream()) { //serialize config WriteConfigTo(mS); //write config mS.Position = 0; using (FileStream fS = new FileStream(blockListConfigFile, FileMode.Create, FileAccess.Write)) { mS.CopyTo(fS); } } _dnsServer.LogManager.Write("DNS Server block list config file was saved: " + blockListConfigFile); } public void SaveConfigFile() { lock (_saveLock) { if (_pendingSave) return; _pendingSave = true; _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void ReadConfigFrom(Stream s, bool isConfigTransfer) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "BL") //format throw new InvalidDataException("DnsServer block list zone file format is invalid."); byte version = bR.ReadByte(); switch (version) { case 1: int count = bR.ReadByte(); string[] blockListUrls = new string[count]; for (int i = 0; i < count; i++) blockListUrls[i] = bR.ReadShortString(); _blockListUpdateIntervalHours = bR.ReadInt32(); DateTime blockListLastUpdatedOn = bR.ReadDateTime(); if (!isConfigTransfer) _blockListLastUpdatedOn = blockListLastUpdatedOn; if (blockListUrls.Length > 0) { //load block list URLs async ThreadPool.QueueUserWorkItem(delegate (object state) { try { LoadBlockLists(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }); } ApplyBlockListUrls(blockListUrls); ApplyBlockListUpdateInterval(); break; default: throw new InvalidDataException("DnsServer block list zone file version not supported."); } } private void WriteConfigTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("BL")); //format bW.Write((byte)1); //version bW.Write(Convert.ToByte(_blockListUrls.Count)); foreach (string blockListUrl in _blockListUrls) bW.WriteShortString(blockListUrl); bW.Write(_blockListUpdateIntervalHours); bW.Write(_blockListLastUpdatedOn); } #endregion #region private internal void UpdateServerDomain() { _soaRecord = new DnsSOARecordData(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, _dnsServer.BlockingAnswerTtl); _nsRecord = new DnsNSRecordData(_dnsServer.ServerDomain); } private string GetBlockListFilePath(Uri blockListUrl) { return Path.Combine(_localCacheFolder, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(blockListUrl.AbsoluteUri))).ToLowerInvariant()); } private static string PopWord(ref string line) { if (line.Length == 0) return line; line = line.TrimStart(_popWordSeperator); int i = line.IndexOfAny(_popWordSeperator); string word; if (i < 0) { word = line; line = ""; } else { word = line.Substring(0, i); line = line.Substring(i + 1); } return word; } private Queue ReadListFile(Uri listUrl, bool isAllowList, out Queue exceptionDomains) { Queue domains = new Queue(); exceptionDomains = new Queue(); try { _dnsServer.LogManager.Write("DNS Server is reading " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri); string listFilePath = GetBlockListFilePath(listUrl); if (listUrl.IsFile) { if (!File.Exists(listFilePath) || (File.GetLastWriteTimeUtc(listUrl.LocalPath) > File.GetLastWriteTimeUtc(listFilePath))) { File.Copy(listUrl.LocalPath, listFilePath, true); _dnsServer.LogManager.Write("DNS Server successfully downloaded " + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri); } } using (FileStream fS = new FileStream(listFilePath, FileMode.Open, FileAccess.Read)) { //parse hosts file and populate block zone StreamReader sR = new StreamReader(fS, true); string line; string firstWord; string secondWord; string hostname; string domain; string options; int i; while (true) { line = sR.ReadLine(); if (line is null) break; //eof line = line.TrimStart(_trimSeperator); if (line.Length == 0) continue; //skip empty line if (line.StartsWith('#') || line.StartsWith('!')) continue; //skip comment line if (line.StartsWith("||")) { //adblock format i = line.IndexOf('^'); if (i > -1) { domain = line.Substring(2, i - 2); options = line.Substring(i + 1); if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) domains.Enqueue(domain.ToLowerInvariant()); } else { domain = line.Substring(2); if (DnsClient.IsDomainNameValid(domain)) domains.Enqueue(domain.ToLowerInvariant()); } } else if (line.StartsWith("@@||")) { //adblock format - exception syntax i = line.IndexOf('^'); if (i > -1) { domain = line.Substring(4, i - 4); options = line.Substring(i + 1); if (((options.Length == 0) || (options.StartsWith('$') && (options.Contains("doc") || options.Contains("all")))) && DnsClient.IsDomainNameValid(domain)) exceptionDomains.Enqueue(domain.ToLowerInvariant()); } else { domain = line.Substring(4); if (DnsClient.IsDomainNameValid(domain)) exceptionDomains.Enqueue(domain.ToLowerInvariant()); } } else { //hosts file format firstWord = PopWord(ref line); if (line.Length == 0) { hostname = firstWord; } else { secondWord = PopWord(ref line); if ((secondWord.Length == 0) || secondWord.StartsWith('#')) hostname = firstWord; else hostname = secondWord; } hostname = hostname.Trim('.').ToLowerInvariant(); switch (hostname) { case "": case "localhost": case "localhost.localdomain": case "local": case "broadcasthost": case "ip6-localhost": case "ip6-loopback": case "ip6-localnet": case "ip6-mcastprefix": case "ip6-allnodes": case "ip6-allrouters": case "ip6-allhosts": continue; //skip these hostnames } if (!DnsClient.IsDomainNameValid(hostname)) continue; if (IPAddress.TryParse(hostname, out _)) continue; //skip line when hostname is IP address domains.Enqueue(hostname); } } } _dnsServer.LogManager.Write("DNS Server read " + (isAllowList ? "allow" : "block") + " list file (" + domains.Count + " domain(s) blocked" + (exceptionDomains.Count > 0 ? ", " + exceptionDomains.Count + " domain(s) allowed" : "") + ") from: " + listUrl.AbsoluteUri); } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server failed to read " + (isAllowList ? "allow" : "block") + " list from: " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); } return domains; } private List IsZoneBlocked(string domain, out string blockedDomain) { domain = domain.ToLowerInvariant(); do { if (_blockListZone.TryGetValue(domain, out List blockLists)) { //found zone blocked blockedDomain = domain; return blockLists; } domain = AuthZoneManager.GetParentZone(domain); } while (domain is not null); blockedDomain = null; return null; } private bool IsZoneAllowed(string domain) { domain = domain.ToLowerInvariant(); do { if (_allowListZone.TryGetValue(domain, out _)) return true; domain = AuthZoneManager.GetParentZone(domain); } while (domain is not null); return false; } private void ApplyBlockListUrls(IReadOnlyList blockListUrls) { bool blockListUrlsUpdated = !blockListUrls.HasSameItems(_blockListUrls); _blockListUrls = blockListUrls; if ((_blockListUpdateIntervalHours > 0) && (_blockListUrls.Count > 0)) { if (_blockListUpdateTimer is null) StartBlockListUpdateTimer(blockListUrlsUpdated); else if (blockListUrlsUpdated) ForceUpdateBlockLists(true); } else { StopBlockListUpdateTimer(); } if (_blockListUrls.Count < 1) Flush(); } private void ApplyBlockListUpdateInterval() { if ((_blockListUpdateIntervalHours > 0) && (_blockListUrls.Count > 0)) { if (_blockListUpdateTimer is null) StartBlockListUpdateTimer(false); } else { StopBlockListUpdateTimer(); } } private void Flush() { _allowListZone = new Dictionary(); _blockListZone = new Dictionary>(); } private async Task UpdateBlockListsAsync(bool forceReload) { bool downloaded = false; bool notModified = false; async Task DownloadListUrlAsync(Uri listUrl, bool isAllowList) { try { _dnsServer.LogManager.Write("DNS Server is downloading " + (isAllowList ? "allow" : "block") + " list: " + listUrl.AbsoluteUri); string listFilePath = GetBlockListFilePath(listUrl); if (listUrl.IsFile) { if (File.Exists(listFilePath)) { if (File.GetLastWriteTimeUtc(listUrl.LocalPath) <= File.GetLastWriteTimeUtc(listFilePath)) { notModified = true; _dnsServer.LogManager.Write("DNS Server successfully checked for a new update of the " + (isAllowList ? "allow" : "block") + " list: " + listUrl.AbsoluteUri); return; } } File.Copy(listUrl.LocalPath, listFilePath, true); downloaded = true; _dnsServer.LogManager.Write("DNS Server successfully downloaded " + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri); } else { HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsServer.Proxy; handler.NetworkType = _dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsServer; using (HttpClient http = new HttpClient(handler)) { if (File.Exists(listFilePath)) http.DefaultRequestHeaders.IfModifiedSince = File.GetLastWriteTimeUtc(listFilePath); HttpResponseMessage httpResponse = await http.GetAsync(listUrl); switch (httpResponse.StatusCode) { case HttpStatusCode.OK: { string listDownloadFilePath = listFilePath + ".downloading"; using (FileStream fS = new FileStream(listDownloadFilePath, FileMode.Create, FileAccess.Write)) { using (Stream httpStream = await httpResponse.Content.ReadAsStreamAsync()) { await httpStream.CopyToAsync(fS); } } File.Move(listDownloadFilePath, listFilePath, true); if (httpResponse.Content.Headers.LastModified != null) File.SetLastWriteTimeUtc(listFilePath, httpResponse.Content.Headers.LastModified.Value.UtcDateTime); downloaded = true; _dnsServer.LogManager.Write("DNS Server successfully downloaded " + (isAllowList ? "allow" : "block") + " list (" + WebUtilities.GetFormattedSize(new FileInfo(listFilePath).Length) + "): " + listUrl.AbsoluteUri); } break; case HttpStatusCode.NotModified: { notModified = true; _dnsServer.LogManager.Write("DNS Server successfully checked for a new update of the " + (isAllowList ? "allow" : "block") + " list: " + listUrl.AbsoluteUri); } break; default: throw new HttpRequestException((int)httpResponse.StatusCode + " " + httpResponse.ReasonPhrase); } } } } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server failed to download " + (isAllowList ? "allow" : "block") + " list and will use previously downloaded file (if available): " + listUrl.AbsoluteUri + "\r\n" + ex.ToString()); } } List tasks = new List(); foreach (string blockListUrl in _blockListUrls) { if (blockListUrl.TrimStart().StartsWith('#')) continue; //skip comment line if (blockListUrl.StartsWith('!')) tasks.Add(DownloadListUrlAsync(new Uri(blockListUrl.Substring(1)), true)); else tasks.Add(DownloadListUrlAsync(new Uri(blockListUrl), false)); } await Task.WhenAll(tasks); if (downloaded || forceReload) { LoadBlockLists(); //force GC collection to remove old zone data from memory quickly GC.Collect(); } return downloaded || notModified; } private void ForceUpdateBlockLists(bool forceReload) { ThreadPool.QueueUserWorkItem(async delegate (object state) { try { if (await UpdateBlockListsAsync(forceReload)) { //block lists were updated //save last updated on time _blockListLastUpdatedOn = DateTime.UtcNow; SaveConfigFile(); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }); } private void StartBlockListUpdateTimer(bool forceUpdateAndReload) { if (_blockListUpdateTimer is null) { if (forceUpdateAndReload) _blockListLastUpdatedOn = default; _blockListUpdateTimer = new Timer(async delegate (object state) { try { if (DateTime.UtcNow > _blockListLastUpdatedOn.AddHours(_blockListUpdateIntervalHours)) { if (await UpdateBlockListsAsync(_blockListLastUpdatedOn == default)) { //block lists were updated //save last updated on time _blockListLastUpdatedOn = DateTime.UtcNow; SaveConfigFile(); } } } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server encountered an error while updating block lists.\r\n" + ex.ToString()); } finally { try { _blockListUpdateTimer?.Change(BLOCK_LIST_UPDATE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } }, null, BLOCK_LIST_UPDATE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void StopBlockListUpdateTimer() { if (_blockListUpdateTimer is not null) { _blockListUpdateTimer.Dispose(); _blockListUpdateTimer = null; } } private void LoadBlockLists() { _dnsServer.LogManager.Write("DNS Server is loading block list zone..."); List allowListUrls = new List(); List blockListUrls = new List(); foreach (string listUri in _blockListUrls) { if (listUri.TrimStart().StartsWith('#')) continue; //skip comment line if (listUri.StartsWith('!')) allowListUrls.Add(new Uri(listUri.Substring(1))); else blockListUrls.Add(new Uri(listUri)); } Dictionary> allowListQueues = new Dictionary>(allowListUrls.Count); Dictionary> blockListQueues = new Dictionary>(blockListUrls.Count); int totalAllowedDomains = 0; int totalBlockedDomains = 0; //read all allow lists in a queue foreach (Uri allowListUrl in allowListUrls) { if (!allowListQueues.ContainsKey(allowListUrl)) { Queue allowListQueue = ReadListFile(allowListUrl, true, out Queue blockListQueue); totalAllowedDomains += allowListQueue.Count; allowListQueues.Add(allowListUrl, allowListQueue); totalBlockedDomains += blockListQueue.Count; blockListQueues.Add(allowListUrl, blockListQueue); } } //read all block lists in a queue foreach (Uri blockListUrl in blockListUrls) { if (!blockListQueues.ContainsKey(blockListUrl)) { Queue blockListQueue = ReadListFile(blockListUrl, false, out Queue allowListQueue); totalBlockedDomains += blockListQueue.Count; blockListQueues.Add(blockListUrl, blockListQueue); totalAllowedDomains += allowListQueue.Count; allowListQueues.Add(blockListUrl, allowListQueue); } } //load block list zone Dictionary allowListZone = new Dictionary(totalAllowedDomains); foreach (KeyValuePair> allowListQueue in allowListQueues) { Queue queue = allowListQueue.Value; while (queue.Count > 0) { string domain = queue.Dequeue(); allowListZone.TryAdd(domain, null); } } Dictionary> blockListZone = new Dictionary>(totalBlockedDomains); foreach (KeyValuePair> blockListQueue in blockListQueues) { Queue queue = blockListQueue.Value; while (queue.Count > 0) { string domain = queue.Dequeue(); if (!blockListZone.TryGetValue(domain, out List blockLists)) { blockLists = new List(2); blockListZone.Add(domain, blockLists); } blockLists.Add(blockListQueue.Key); } } //set new allowed and blocked zones _allowListZone = allowListZone; _blockListZone = blockListZone; _dnsServer.LogManager.Write("DNS Server block list zone was loaded successfully."); } #endregion #region public public bool IsAllowed(DnsDatagram request) { if (_allowListZone.Count < 1) return false; return IsZoneAllowed(request.Question[0].Name); } public DnsDatagram Query(DnsDatagram request) { if (_blockListZone.Count < 1) return null; DnsQuestionRecord question = request.Question[0]; List blockLists = IsZoneBlocked(question.Name, out string blockedDomain); if (blockLists is null) return null; //zone not blocked //zone is blocked if (_dnsServer.AllowTxtBlockingReport && (question.Type == DnsResourceRecordType.TXT)) { //return meta data DnsResourceRecord[] answer = new DnsResourceRecord[blockLists.Count]; for (int i = 0; i < answer.Length; i++) answer[i] = new DnsResourceRecord(question.Name, DnsResourceRecordType.TXT, question.Class, _dnsServer.BlockingAnswerTtl, new DnsTXTRecordData("source=block-list-zone; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain)); return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer); } else { EDnsOption[] options = null; if (_dnsServer.AllowTxtBlockingReport && (request.EDNS is not null)) { options = new EDnsOption[blockLists.Count]; for (int i = 0; i < options.Length; i++) options[i] = new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.Blocked, "source=block-list-zone; blockListUrl=" + blockLists[i].AbsoluteUri + "; domain=" + blockedDomain)); } IReadOnlyCollection aRecords; IReadOnlyCollection aaaaRecords; switch (_dnsServer.BlockingType) { case DnsServerBlockingType.AnyAddress: aRecords = _aRecords; aaaaRecords = _aaaaRecords; break; case DnsServerBlockingType.CustomAddress: aRecords = _dnsServer.CustomBlockingARecords; aaaaRecords = _dnsServer.CustomBlockingAAAARecords; break; case DnsServerBlockingType.NxDomain: string parentDomain = AuthZoneManager.GetParentZone(blockedDomain); if (parentDomain is null) parentDomain = string.Empty; return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NxDomain, request.Question, null, [new DnsResourceRecord(parentDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)], null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options); default: throw new InvalidOperationException(); } IReadOnlyList answer = null; IReadOnlyList authority = null; switch (question.Type) { case DnsResourceRecordType.A: { if (aRecords.Count > 0) { DnsResourceRecord[] rrList = new DnsResourceRecord[aRecords.Count]; int i = 0; foreach (DnsARecordData record in aRecords) rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.A, question.Class, _dnsServer.BlockingAnswerTtl, record); answer = rrList; } else { authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)]; } } break; case DnsResourceRecordType.AAAA: { if (aaaaRecords.Count > 0) { DnsResourceRecord[] rrList = new DnsResourceRecord[aaaaRecords.Count]; int i = 0; foreach (DnsAAAARecordData record in aaaaRecords) rrList[i++] = new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, question.Class, _dnsServer.BlockingAnswerTtl, record); answer = rrList; } else { authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)]; } } break; case DnsResourceRecordType.NS: if (question.Name.Equals(blockedDomain, StringComparison.OrdinalIgnoreCase)) answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.NS, question.Class, _dnsServer.BlockingAnswerTtl, _nsRecord)]; else authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)]; break; case DnsResourceRecordType.SOA: answer = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)]; break; default: authority = [new DnsResourceRecord(blockedDomain, DnsResourceRecordType.SOA, question.Class, _dnsServer.BlockingAnswerTtl, _soaRecord)]; break; } return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, options); } } public void ForceUpdateBlockLists() { ForceUpdateBlockLists(false); } public void TemporaryDisableBlocking(int minutes, IPEndPoint userEP, string username) { Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer; if (temporaryDisableBlockingTimer is not null) temporaryDisableBlockingTimer.Dispose(); Timer newTemporaryDisableBlockingTimer = new Timer(delegate (object state) { try { _dnsServer.EnableBlocking = true; _dnsServer.LogManager.Write(userEP, "[" + username + "] Blocking was enabled after " + minutes + " minute(s) being temporarily disabled."); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } }); Timer originalTimer = Interlocked.CompareExchange(ref _temporaryDisableBlockingTimer, newTemporaryDisableBlockingTimer, temporaryDisableBlockingTimer); if (ReferenceEquals(originalTimer, temporaryDisableBlockingTimer)) { newTemporaryDisableBlockingTimer.Change(minutes * 60 * 1000, Timeout.Infinite); _dnsServer.EnableBlocking = false; _temporaryDisableBlockingTill = DateTime.UtcNow.AddMinutes(minutes); _dnsServer.LogManager.Write(userEP, "[" + username + "] Blocking was temporarily disabled for " + minutes + " minute(s)."); } else { newTemporaryDisableBlockingTimer.Dispose(); } } public void StopTemporaryDisableBlockingTimer() { Timer temporaryDisableBlockingTimer = _temporaryDisableBlockingTimer; if (temporaryDisableBlockingTimer is not null) temporaryDisableBlockingTimer.Dispose(); } #endregion #region properties public IReadOnlyList BlockListUrls { get { return _blockListUrls; } set { if (value is null) { value = []; } else if (value.Count > 255) { throw new ArgumentException("Cannot configure more than 255 block list URLs.", nameof(BlockListUrls)); } else { List uniqueList = new List(value.Count); int commentCount = 0; foreach (string url in value) { if (url.Length > 255) throw new ArgumentException("Block list URL (or comment line) length cannot exceed 255 characters.", nameof(BlockListUrls)); if (url.TrimStart().StartsWith('#')) { uniqueList.Add(url); commentCount++; continue; } if (!uniqueList.Contains(url)) uniqueList.Add(url); } if (uniqueList.Count == commentCount) uniqueList = []; value = uniqueList; } ApplyBlockListUrls(value); } } public int BlockListUpdateIntervalHours { get { return _blockListUpdateIntervalHours; } set { if ((value < 0) || (value > 168)) throw new ArgumentOutOfRangeException(nameof(BlockListUpdateIntervalHours), "Value must be between 1 hour and 168 hours (7 days) or 0 to disable automatic update."); _blockListUpdateIntervalHours = value; ApplyBlockListUpdateInterval(); } } public bool BlockListUpdateEnabled { get { return _blockListUpdateTimer is not null; } } public DateTime BlockListLastUpdatedOn { get { return _blockListLastUpdatedOn; } internal set { _blockListLastUpdatedOn = value; } } public DateTime TemporaryDisableBlockingTill { get { return _temporaryDisableBlockingTill; } } public int TotalZonesAllowed { get { return _allowListZone.Count; } } public int TotalZonesBlocked { get { return _blockListZone.Count; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ZoneManagers/BlockedZoneManager.cs ================================================ /* 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.Dns.ResourceRecords; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ZoneManagers { public sealed class BlockedZoneManager { #region variables readonly DnsServer _dnsServer; AuthZoneManager _zoneManager; readonly DnsSOARecordDataExtended _soaRecord; readonly DnsNSRecordDataExtended _nsRecord; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor public BlockedZoneManager(DnsServer dnsServer) { _dnsServer = dnsServer; _zoneManager = new AuthZoneManager(_dnsServer); _soaRecord = new DnsSOARecordDataExtended(_dnsServer.ServerDomain, _dnsServer.ResponsiblePerson.Address, 1, 14400, 3600, 604800, _dnsServer.BlockingAnswerTtl); _nsRecord = new DnsNSRecordDataExtended(_dnsServer.ServerDomain); _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { if (_pendingSave) { try { SaveZoneFileInternal(); _pendingSave = false; } catch (Exception ex) { _dnsServer.LogManager.Write(ex); //set timer to retry again _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveZoneFileInternal(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { _pendingSave = false; } } } _disposed = true; } #endregion #region zone file public void LoadBlockedZoneFile() { string blockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "blocked.config"); try { string oldCustomBlockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "custom-blocked.config"); if (File.Exists(oldCustomBlockedZoneFile)) { if (File.Exists(blockedZoneFile)) File.Delete(blockedZoneFile); File.Move(oldCustomBlockedZoneFile, blockedZoneFile); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } try { using (FileStream fS = new FileStream(blockedZoneFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS); } _dnsServer.LogManager.Write("DNS Server blocked zone file was loaded: " + blockedZoneFile); } catch (FileNotFoundException) { SaveZoneFileInternal(); } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server encountered an error while loading blocked zone file: " + blockedZoneFile + "\r\n" + ex.ToString()); } } public void LoadBlockedZone(Stream s) { lock (_saveLock) { ReadConfigFrom(s); SaveZoneFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void SaveZoneFileInternal() { string blockedZoneFile = Path.Combine(_dnsServer.ConfigFolder, "blocked.config"); using (FileStream fS = new FileStream(blockedZoneFile, FileMode.Create, FileAccess.Write)) { WriteConfigTo(fS); } _dnsServer.LogManager.Write("DNS Server blocked zone file was saved: " + blockedZoneFile); } public void SaveZoneFile() { lock (_saveLock) { if (_pendingSave) return; _pendingSave = true; _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void ReadConfigFrom(Stream s) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "BZ") //format throw new InvalidDataException("DnsServer blocked zone file format is invalid."); byte version = bR.ReadByte(); switch (version) { case 1: int length = bR.ReadInt32(); int i = 0; AuthZoneManager zoneManager = new AuthZoneManager(_dnsServer); zoneManager.LoadSpecialPrimaryZones(delegate () { if (i++ < length) return bR.ReadShortString(); return null; }, _soaRecord, _nsRecord); _zoneManager = zoneManager; break; default: throw new InvalidDataException("DnsServer blocked zone file version not supported."); } } private void WriteConfigTo(Stream s) { IReadOnlyList blockedZones = _zoneManager.GetAllZones(); BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("BZ")); //format bW.Write((byte)1); //version bW.Write(blockedZones.Count); foreach (AuthZoneInfo zone in blockedZones) bW.WriteShortString(zone.Name); } #endregion #region private internal void UpdateServerDomain() { _soaRecord.UpdatePrimaryNameServerAndMinimum(_dnsServer.ServerDomain, _dnsServer.BlockingAnswerTtl); _nsRecord.UpdateNameServer(_dnsServer.ServerDomain); } #endregion #region public public void ImportZones(string[] domains) { _zoneManager.LoadSpecialPrimaryZones(domains, _soaRecord, _nsRecord); } public bool BlockZone(string domain) { if (_zoneManager.CreateSpecialPrimaryZone(domain, _soaRecord, _nsRecord) != null) return true; return false; } public bool DeleteZone(string domain) { if (_zoneManager.DeleteZone(domain)) return true; return false; } public void Flush() { _zoneManager.Flush(); } public IReadOnlyList GetAllZones() { return _zoneManager.GetAllZones(); } public void ListAllRecords(string domain, List records) { _zoneManager.ListAllRecords(domain, domain, records); } public void ListSubDomains(string domain, List subDomains) { _zoneManager.ListSubDomains(domain, subDomains); } public DnsDatagram Query(DnsDatagram request) { if (_zoneManager.TotalZones < 1) return null; return _zoneManager.Query(request, false); } #endregion #region properties internal DnsSOARecordData DnsSOARecord { get { return _soaRecord; } } public int TotalZonesBlocked { get { return _zoneManager.TotalZones; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/ZoneManagers/CacheZoneManager.cs ================================================ /* 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.Dns.ResourceRecords; using DnsServerCore.Dns.Trees; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.ZoneManagers { public sealed class CacheZoneManager : DnsCache, IDisposable { #region variables public const uint FAILURE_RECORD_TTL = 10u; public const uint NEGATIVE_RECORD_TTL = 300u; public const uint MINIMUM_RECORD_TTL = 10u; public const uint MAXIMUM_RECORD_TTL = 7 * 24 * 60 * 60; public const uint SERVE_STALE_TTL = 3 * 24 * 60 * 60; //3 days serve stale ttl as per https://www.rfc-editor.org/rfc/rfc8767.html suggestion public const uint SERVE_STALE_ANSWER_TTL = 30; //as per https://www.rfc-editor.org/rfc/rfc8767.html suggestion public const uint SERVE_STALE_RESET_TTL = 30; //as per https://www.rfc-editor.org/rfc/rfc8767.html suggestion const uint SERVE_STALE_MIN_RESET_TTL = 10; const uint SERVE_STALE_MAX_RESET_TTL = 900; readonly DnsServer _dnsServer; readonly CacheZoneTree _root = new CacheZoneTree(); uint _serveStaleResetTtl = SERVE_STALE_RESET_TTL; long _maximumEntries; long _totalEntries; Timer _cacheMaintenanceTimer; readonly object _cacheMaintenanceTimerLock = new object(); const int CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL = 5 * 60 * 1000; const int CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL = 5 * 60 * 1000; #endregion #region constructor public CacheZoneManager(DnsServer dnsServer) : base(FAILURE_RECORD_TTL, NEGATIVE_RECORD_TTL, MINIMUM_RECORD_TTL, MAXIMUM_RECORD_TTL, SERVE_STALE_TTL, SERVE_STALE_ANSWER_TTL) { _dnsServer = dnsServer; _cacheMaintenanceTimer = new Timer(CacheMaintenanceTimerCallback, null, CACHE_MAINTENANCE_TIMER_INITIAL_INTEVAL, Timeout.Infinite); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; lock (_cacheMaintenanceTimerLock) { if (_cacheMaintenanceTimer is not null) { _cacheMaintenanceTimer.Dispose(); _cacheMaintenanceTimer = null; } } _disposed = true; } #endregion #region zone file public void LoadCacheZoneFile() { string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, "cache.bin"); if (!File.Exists(cacheZoneFile)) return; _dnsServer.LogManager.Write("Loading DNS Cache from disk..."); using (FileStream fS = new FileStream(cacheZoneFile, FileMode.Open, FileAccess.Read)) { BinaryReader bR = new BinaryReader(fS); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "CZ") throw new InvalidDataException("CacheZoneManager format is invalid."); int version = bR.ReadByte(); switch (version) { case 1: int addedEntries = 0; try { bool serveStale = _dnsServer.ServeStale; while (bR.BaseStream.Position < bR.BaseStream.Length) { CacheZone zone = CacheZone.ReadFrom(bR, serveStale); if (!zone.IsEmpty) { if (_root.TryAdd(zone.Name, zone)) addedEntries += zone.TotalEntries; } } } finally { if (addedEntries > 0) Interlocked.Add(ref _totalEntries, addedEntries); } break; default: throw new InvalidDataException("CacheZoneManager format version not supported: " + version); } } _dnsServer.LogManager.Write("DNS Cache was loaded from disk successfully."); } public void SaveCacheZoneFile() { _dnsServer.LogManager.Write("Saving DNS Cache to disk..."); string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, "cache.bin"); using (FileStream fS = new FileStream(cacheZoneFile, FileMode.Create, FileAccess.Write)) { BinaryWriter bW = new BinaryWriter(fS); bW.Write(Encoding.ASCII.GetBytes("CZ")); //format bW.Write((byte)1); //version foreach (CacheZone zone in _root) zone.WriteTo(bW); } _dnsServer.LogManager.Write("DNS Cache was saved to disk successfully."); } public void DeleteCacheZoneFile() { string cacheZoneFile = Path.Combine(_dnsServer.ConfigFolder, "cache.bin"); if (File.Exists(cacheZoneFile)) File.Delete(cacheZoneFile); } #endregion #region protected protected override void CacheRecords(IReadOnlyList resourceRecords, NetworkAddress eDnsClientSubnet, DnsDatagramMetadata responseMetadata) { List dnameRecords = null; //read and set glue records from base class; also collect any DNAME records found foreach (DnsResourceRecord resourceRecord in resourceRecords) { DnsResourceRecordInfo recordInfo = GetRecordInfo(resourceRecord); IReadOnlyList glueRecords = recordInfo.GlueRecords; IReadOnlyList rrsigRecords = recordInfo.RRSIGRecords; IReadOnlyList nsecRecords = recordInfo.NSECRecords; CacheRecordInfo rrInfo = resourceRecord.GetCacheRecordInfo(); rrInfo.GlueRecords = glueRecords; rrInfo.RRSIGRecords = rrsigRecords; rrInfo.NSECRecords = nsecRecords; rrInfo.EDnsClientSubnet = eDnsClientSubnet; rrInfo.ResponseMetadata = responseMetadata; if (glueRecords is not null) { foreach (DnsResourceRecord glueRecord in glueRecords) { IReadOnlyList glueRRSIGRecords = GetRecordInfo(glueRecord).RRSIGRecords; if (glueRRSIGRecords is not null) glueRecord.GetCacheRecordInfo().RRSIGRecords = glueRRSIGRecords; } } if (nsecRecords is not null) { foreach (DnsResourceRecord nsecRecord in nsecRecords) { IReadOnlyList nsecRRSIGRecords = GetRecordInfo(nsecRecord).RRSIGRecords; if (nsecRRSIGRecords is not null) nsecRecord.GetCacheRecordInfo().RRSIGRecords = nsecRRSIGRecords; } } if (resourceRecord.Type == DnsResourceRecordType.DNAME) { if (dnameRecords is null) dnameRecords = new List(1); dnameRecords.Add(resourceRecord); } } if (resourceRecords.Count == 1) { DnsResourceRecord resourceRecord = resourceRecords[0]; CacheZone zone = _root.GetOrAdd(resourceRecord.Name, delegate (string key) { return new CacheZone(resourceRecord.Name, 1); }); if (zone.SetRecords(resourceRecord.Type, resourceRecords, _dnsServer.ServeStale)) Interlocked.Increment(ref _totalEntries); } else { Dictionary>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(resourceRecords); bool serveStale = _dnsServer.ServeStale; int addedEntries = 0; //add grouped records foreach (KeyValuePair>> groupedByTypeRecords in groupedByDomainRecords) { if (dnameRecords is not null) { bool foundSynthesizedCNAME = false; foreach (DnsResourceRecord dnameRecord in dnameRecords) { if (groupedByTypeRecords.Key.EndsWith("." + dnameRecord.Name, StringComparison.OrdinalIgnoreCase)) { foundSynthesizedCNAME = true; break; } } if (foundSynthesizedCNAME) continue; //do not cache synthesized CNAME } CacheZone zone = _root.GetOrAdd(groupedByTypeRecords.Key, delegate (string key) { return new CacheZone(groupedByTypeRecords.Key, groupedByTypeRecords.Value.Count); }); foreach (KeyValuePair> groupedRecords in groupedByTypeRecords.Value) { if (zone.SetRecords(groupedRecords.Key, groupedRecords.Value, serveStale)) addedEntries++; } } if (addedEntries > 0) Interlocked.Add(ref _totalEntries, addedEntries); } } #endregion #region private private void CacheMaintenanceTimerCallback(object state) { try { RemoveExpiredRecords(); //force GC collection to remove old cache data from memory quickly GC.Collect(); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { lock (_cacheMaintenanceTimerLock) { _cacheMaintenanceTimer?.Change(CACHE_MAINTENANCE_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } } } private static IReadOnlyList AddDSRecordsTo(CacheZone delegation, bool serveStale, IReadOnlyList nsRecords, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet) { IReadOnlyList records = delegation.QueryRecords(DnsResourceRecordType.DS, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.DS)) { List newNSRecords = new List(nsRecords.Count + records.Count); newNSRecords.AddRange(nsRecords); newNSRecords.AddRange(records); return newNSRecords; } //no DS records found check for NSEC records IReadOnlyList nsecRecords = nsRecords[0].GetCacheRecordInfo().NSECRecords; if (nsecRecords is not null) { List newNSRecords = new List(nsRecords.Count + nsecRecords.Count); newNSRecords.AddRange(nsRecords); newNSRecords.AddRange(nsecRecords); return newNSRecords; } //found nothing; return original NS records return nsRecords; } private static void AddRRSIGRecords(IReadOnlyList answer, out IReadOnlyList newAnswer, out IReadOnlyList newAuthority) { List newAnswerList = new List(answer.Count * 2); List newAuthorityList = null; foreach (DnsResourceRecord record in answer) { if (record.Type == DnsResourceRecordType.RRSIG) continue; //skip RRSIG to avoid duplicates newAnswerList.Add(record); CacheRecordInfo rrInfo = record.GetCacheRecordInfo(); IReadOnlyList rrsigRecords = rrInfo.RRSIGRecords; if (rrsigRecords is not null) { newAnswerList.AddRange(rrsigRecords); foreach (DnsResourceRecord rrsigRecord in rrsigRecords) { if (!DnsRRSIGRecordData.IsWildcard(rrsigRecord)) continue; //add NSEC/NSEC3 for the wildcard proof if (newAuthorityList is null) newAuthorityList = new List(2); IReadOnlyList nsecRecords = rrInfo.NSECRecords; if (nsecRecords is not null) { foreach (DnsResourceRecord nsecRecord in nsecRecords) { newAuthorityList.Add(nsecRecord); IReadOnlyList nsecRRSIGRecords = nsecRecord.GetCacheRecordInfo().RRSIGRecords; if (nsecRRSIGRecords is not null) newAuthorityList.AddRange(nsecRRSIGRecords); } } } } } newAnswer = newAnswerList; newAuthority = newAuthorityList; } private void ResolveCNAME(DnsQuestionRecord question, DnsResourceRecord lastCNAME, bool serveStale, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, List answerRecords) { int queryCount = 0; do { string cnameDomain = (lastCNAME.RDATA as DnsCNAMERecordData).Domain; if (lastCNAME.Name.Equals(cnameDomain, StringComparison.OrdinalIgnoreCase)) break; //loop detected if (!_root.TryGet(cnameDomain, out CacheZone cacheZone)) break; IReadOnlyList records = cacheZone.QueryRecords(question.Type == DnsResourceRecordType.NS ? DnsResourceRecordType.CHILD_NS : question.Type, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if (records.Count < 1) break; DnsResourceRecord lastRR = records[records.Count - 1]; if (lastRR.Type != DnsResourceRecordType.CNAME) { answerRecords.AddRange(records); break; } foreach (DnsResourceRecord answerRecord in answerRecords) { if (answerRecord.Type != DnsResourceRecordType.CNAME) continue; if (answerRecord.RDATA.Equals(lastRR.RDATA)) return; //loop detected } answerRecords.AddRange(records); lastCNAME = lastRR; } while (++queryCount < DnsServer.MAX_CNAME_HOPS); } private bool DoDNAMESubstitution(DnsQuestionRecord question, IReadOnlyList answer, bool serveStale, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, out IReadOnlyList newAnswer) { DnsResourceRecord dnameRR = answer[0]; string result = (dnameRR.RDATA as DnsDNAMERecordData).Substitute(question.Name, dnameRR.Name); if (DnsClient.IsDomainNameValid(result)) { DnsResourceRecord cnameRR = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, dnameRR.TTL, new DnsCNAMERecordData(result)); List list = new List(5) { dnameRR, cnameRR }; ResolveCNAME(question, cnameRR, serveStale, eDnsClientSubnet, advancedForwardingClientSubnet, list); newAnswer = list; return true; } else { newAnswer = answer; return false; } } private List GetAdditionalRecords(IReadOnlyList refRecords, bool serveStale, bool dnssecOk, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet) { List additionalRecords = new List(); foreach (DnsResourceRecord refRecord in refRecords) { switch (refRecord.Type) { case DnsResourceRecordType.NS: if (refRecord.RDATA is DnsNSRecordData ns) ResolveAdditionalRecords(refRecord, ns.NameServer, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords); break; case DnsResourceRecordType.MX: if (refRecord.RDATA is DnsMXRecordData mx) ResolveAdditionalRecords(refRecord, mx.Exchange, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords); break; case DnsResourceRecordType.SRV: if (refRecord.RDATA is DnsSRVRecordData srv) ResolveAdditionalRecords(refRecord, srv.Target, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords); break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: if (refRecord.RDATA is DnsSVCBRecordData svcb) { string targetName = svcb.TargetName; if (svcb.SvcPriority == 0) { //For AliasMode SVCB RRs, a TargetName of "." indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12] if ((targetName.Length == 0) || targetName.Equals(refRecord.Name, StringComparison.OrdinalIgnoreCase)) break; } else { //For ServiceMode SVCB RRs, if TargetName has the value ".", then the owner name of this record MUST be used as the effective TargetName [draft-ietf-dnsop-svcb-https-12] if (targetName.Length == 0) targetName = refRecord.Name; } ResolveAdditionalRecords(refRecord, targetName, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet, additionalRecords); } break; } } return additionalRecords; } private void ResolveAdditionalRecords(DnsResourceRecord refRecord, string domain, bool serveStale, bool dnssecOk, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet, List additionalRecords) { IReadOnlyList glueRecords = refRecord.GetCacheRecordInfo().GlueRecords; if (glueRecords is not null) { bool added = false; foreach (DnsResourceRecord glueRecord in glueRecords) { if (!glueRecord.IsStale) { added = true; additionalRecords.Add(glueRecord); if (dnssecOk) { IReadOnlyList rrsigRecords = glueRecord.GetCacheRecordInfo().RRSIGRecords; if (rrsigRecords is not null) additionalRecords.AddRange(rrsigRecords); } } } if (added) return; } int count = 0; while ((count++ < DnsServer.MAX_CNAME_HOPS) && _root.TryGet(domain, out CacheZone cacheZone)) { if (((refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) && ((refRecord.RDATA as DnsSVCBRecordData).SvcPriority == 0)) { //resolve SVCB/HTTPS for Alias mode refRecord IReadOnlyList records = cacheZone.QueryRecords(refRecord.Type, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((records.Count > 0) && (records[0].Type == refRecord.Type) && (records[0].RDATA is DnsSVCBRecordData svcb)) { additionalRecords.AddRange(records); string targetName = svcb.TargetName; if (svcb.SvcPriority == 0) { //Alias mode if ((targetName.Length == 0) || targetName.Equals(records[0].Name, StringComparison.OrdinalIgnoreCase)) break; //For AliasMode SVCB RRs, a TargetName of "." indicates that the service is not available or does not exist [draft-ietf-dnsop-svcb-https-12] foreach (DnsResourceRecord additionalRecord in additionalRecords) { if (additionalRecord.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase)) return; //loop detected } //continue to resolve SVCB/HTTPS further domain = targetName; refRecord = records[0]; continue; } else { //Service mode if (targetName.Length > 0) { //continue to resolve A/AAAA for target name domain = targetName; refRecord = records[0]; continue; } //resolve A/AAAA below } } } bool hasA = false; bool hasAAAA = false; if ((refRecord.Type == DnsResourceRecordType.SRV) || (refRecord.Type == DnsResourceRecordType.SVCB) || (refRecord.Type == DnsResourceRecordType.HTTPS)) { foreach (DnsResourceRecord additionalRecord in additionalRecords) { if (additionalRecord.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) { switch (additionalRecord.Type) { case DnsResourceRecordType.A: hasA = true; break; case DnsResourceRecordType.AAAA: hasAAAA = true; break; } } if (hasA && hasAAAA) break; } } if (!hasA) { IReadOnlyList records = cacheZone.QueryRecords(DnsResourceRecordType.A, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.A)) additionalRecords.AddRange(records); } if (!hasAAAA) { IReadOnlyList records = cacheZone.QueryRecords(DnsResourceRecordType.AAAA, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((records.Count > 0) && (records[0].Type == DnsResourceRecordType.AAAA)) additionalRecords.AddRange(records); } break; } } private int RemoveExpiredRecordsInternal(bool serveStale, long minimumEntriesToRemove) { int removedEntries = 0; foreach (CacheZone zone in _root) { removedEntries += zone.RemoveExpiredRecords(serveStale); if (zone.IsEmpty) _root.TryRemove(zone.Name, out _); //remove empty zone if ((minimumEntriesToRemove > 0) && (removedEntries >= minimumEntriesToRemove)) break; } if (removedEntries > 0) { long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries); if (totalEntries < 0) Interlocked.Add(ref _totalEntries, -totalEntries); } return removedEntries; } private int RemoveLeastUsedRecordsInternal(DateTime cutoff, long minimumEntriesToRemove) { int removedEntries = 0; foreach (CacheZone zone in _root) { removedEntries += zone.RemoveLeastUsedRecords(cutoff); if (zone.IsEmpty) _root.TryRemove(zone.Name, out _); //remove empty zone if ((minimumEntriesToRemove > 0) && (removedEntries >= minimumEntriesToRemove)) break; } if (removedEntries > 0) { long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries); if (totalEntries < 0) Interlocked.Add(ref _totalEntries, -totalEntries); } return removedEntries; } #endregion #region public public override void RemoveExpiredRecords() { bool serveStale = _dnsServer.ServeStale; //remove expired records/expired stale records RemoveExpiredRecordsInternal(serveStale, 0); if (_maximumEntries < 1) return; //cache limit feature disabled //find minimum entries to remove long minimumEntriesToRemove = _totalEntries - _maximumEntries; if (minimumEntriesToRemove < 1) return; //no need to remove //remove stale records if they exist if (serveStale) minimumEntriesToRemove -= RemoveExpiredRecordsInternal(false, minimumEntriesToRemove); if (minimumEntriesToRemove < 1) return; //task completed //remove least recently used records for (int seconds = 86400; seconds > 0; seconds /= 2) { DateTime cutoff = DateTime.UtcNow.AddSeconds(-seconds); minimumEntriesToRemove -= RemoveLeastUsedRecordsInternal(cutoff, minimumEntriesToRemove); if (minimumEntriesToRemove < 1) break; //task completed } } public void DeleteEDnsClientSubnetData() { int removedEntries = 0; foreach (CacheZone zone in _root) { removedEntries += zone.DeleteEDnsClientSubnetData(); if (zone.IsEmpty) _root.TryRemove(zone.Name, out _); //remove empty zone } if (removedEntries > 0) { long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries); if (totalEntries < 0) Interlocked.Add(ref _totalEntries, -totalEntries); } } public override void Flush() { _root.Clear(); long totalEntries = _totalEntries; totalEntries = Interlocked.Add(ref _totalEntries, -totalEntries); if (totalEntries < 0) Interlocked.Add(ref _totalEntries, -totalEntries); } public bool DeleteZone(string domain) { if (_root.TryRemoveTree(domain, out _, out int removedEntries)) { if (removedEntries > 0) { long totalEntries = Interlocked.Add(ref _totalEntries, -removedEntries); if (totalEntries < 0) Interlocked.Add(ref _totalEntries, -totalEntries); } return true; } return false; } public void ListSubDomains(string domain, List subDomains) { _root.ListSubDomains(domain, subDomains); } public void ListAllRecords(string domain, List records) { if (_root.TryGet(domain, out CacheZone zone)) zone.ListAllRecords(records); } public Task QueryClosestDelegationAsync(DnsDatagram request) { DnsQuestionRecord question = request.Question[0]; string domain = question.Name; NetworkAddress eDnsClientSubnet = null; bool advancedForwardingClientSubnet = false; { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet; } } if (question.Type == DnsResourceRecordType.DS) { //find parent delegation domain = AuthZoneManager.GetParentZone(question.Name); if (domain is null) return Task.FromResult(null); //dont find NS for root } do { _ = _root.FindZone(domain, out _, out CacheZone delegation); if (delegation is null) return Task.FromResult(null); //return closest name servers in delegation IReadOnlyList closestAuthority = delegation.QueryRecords(DnsResourceRecordType.NS, false, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((closestAuthority.Count == 0) && (delegation.Name.Length == 0)) closestAuthority = delegation.QueryRecords(DnsResourceRecordType.CHILD_NS, false, true, eDnsClientSubnet, advancedForwardingClientSubnet); //root zone case if ((closestAuthority.Count > 0) && (closestAuthority[0].Type == DnsResourceRecordType.NS)) { if (request.DnssecOk) { if (closestAuthority[0].DnssecStatus != DnssecStatus.Disabled) //dont return records with disabled status { closestAuthority = AddDSRecordsTo(delegation, false, closestAuthority, eDnsClientSubnet, advancedForwardingClientSubnet); IReadOnlyList additional = GetAdditionalRecords(closestAuthority, false, true, eDnsClientSubnet, advancedForwardingClientSubnet); return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, closestAuthority, additional)); } } else { IReadOnlyList additional = GetAdditionalRecords(closestAuthority, false, false, eDnsClientSubnet, advancedForwardingClientSubnet); return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, DnsResponseCode.NoError, request.Question, null, closestAuthority, additional)); } } domain = AuthZoneManager.GetParentZone(delegation.Name); } while (domain is not null); //no cached delegation found return Task.FromResult(null); } public override Task QueryAsync(DnsDatagram request, bool serveStale = false, bool findClosestNameServers = false, bool resetExpiry = false) { DnsQuestionRecord question = request.Question[0]; NetworkAddress eDnsClientSubnet = null; bool advancedForwardingClientSubnet = false; { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(); if (requestECS is not null) { eDnsClientSubnet = new NetworkAddress(requestECS.Address, requestECS.SourcePrefixLength); advancedForwardingClientSubnet = requestECS.AdvancedForwardingClientSubnet; } } CacheZone zone; CacheZone closest = null; CacheZone delegation = null; if (findClosestNameServers) { zone = _root.FindZone(question.Name, out closest, out delegation); } else { if (!_root.TryGet(question.Name, out zone)) _ = _root.FindZone(question.Name, out closest, out _); //zone not found; attempt to find closest } bool dnssecOk = request.DnssecOk; if (zone is not null) { //zone found IReadOnlyList answer = zone.QueryRecords(question.Type == DnsResourceRecordType.NS ? DnsResourceRecordType.CHILD_NS : question.Type, serveStale, false, eDnsClientSubnet, advancedForwardingClientSubnet); if (answer.Count > 0) { //answer found in cache DnsResourceRecord firstRR = answer[0]; if (firstRR.RDATA is DnsSpecialCacheRecordData dnsSpecialCacheRecord) { if (dnssecOk) { foreach (DnsResourceRecord originalAuthority in dnsSpecialCacheRecord.OriginalAuthority) { if (originalAuthority.DnssecStatus == DnssecStatus.Disabled) goto beforeFindClosestNameServers; //dont return answer with disabled status } } if (resetExpiry) { if (firstRR.IsStale) firstRR.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767 if (dnsSpecialCacheRecord.Authority is not null) { foreach (DnsResourceRecord record in dnsSpecialCacheRecord.Authority) { if (record.IsStale) record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767 } } } IReadOnlyList specialOptions; if (firstRR.WasExpiryReset || firstRR.IsStale) { List newOptions = new List(dnsSpecialCacheRecord.EDnsOptions.Count + 1); newOptions.AddRange(dnsSpecialCacheRecord.EDnsOptions); if (dnsSpecialCacheRecord.RCODE == DnsResponseCode.NxDomain) newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleNxDomainAnswer, firstRR.Name.ToLowerInvariant() + " " + firstRR.Type.ToString() + " " + firstRR.Class.ToString()))); else newOptions.Add(new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleAnswer, firstRR.Name.ToLowerInvariant() + " " + firstRR.Type.ToString() + " " + firstRR.Class.ToString()))); specialOptions = newOptions; } else { specialOptions = dnsSpecialCacheRecord.EDnsOptions; } if (eDnsClientSubnet is not null) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true); if (requestECS is not null) { NetworkAddress recordECS = firstRR.GetCacheRecordInfo().EDnsClientSubnet; if (recordECS is not null) { EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, recordECS.PrefixLength, requestECS.Address); if ((specialOptions is null) || (specialOptions.Count == 0)) { specialOptions = ecsOption; } else { List newOptions = new List(specialOptions.Count + 1); newOptions.AddRange(specialOptions); newOptions.Add(ecsOption[0]); specialOptions = newOptions; } } } } if (dnssecOk) { bool authenticData; switch (dnsSpecialCacheRecord.Type) { case DnsSpecialCacheRecordType.NegativeCache: authenticData = true; break; default: authenticData = false; break; } if (request.CheckingDisabled) return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, authenticData, true, dnsSpecialCacheRecord.OriginalRCODE, request.Question, dnsSpecialCacheRecord.OriginalAnswer, dnsSpecialCacheRecord.OriginalAuthority, dnsSpecialCacheRecord.OriginalAdditional, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, specialOptions)); else return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, authenticData, false, dnsSpecialCacheRecord.RCODE, request.Question, dnsSpecialCacheRecord.Answer, dnsSpecialCacheRecord.Authority, null, _dnsServer.UdpPayloadSize, EDnsHeaderFlags.DNSSEC_OK, specialOptions)); } else { if (request.CheckingDisabled) return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, true, dnsSpecialCacheRecord.OriginalRCODE, request.Question, dnsSpecialCacheRecord.OriginalNoDnssecAnswer, dnsSpecialCacheRecord.OriginalNoDnssecAuthority, dnsSpecialCacheRecord.OriginalAdditional, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, specialOptions)); else return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, false, dnsSpecialCacheRecord.RCODE, request.Question, dnsSpecialCacheRecord.NoDnssecAnswer, dnsSpecialCacheRecord.NoDnssecAuthority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, EDnsHeaderFlags.None, specialOptions)); } } DnsResourceRecord lastRR = answer[answer.Count - 1]; if ((lastRR.Type != question.Type) && (lastRR.Type == DnsResourceRecordType.CNAME) && (question.Type != DnsResourceRecordType.ANY)) { List newAnswers = new List(answer.Count + 3); newAnswers.AddRange(answer); ResolveCNAME(question, lastRR, serveStale, eDnsClientSubnet, advancedForwardingClientSubnet, newAnswers); answer = newAnswers; } IReadOnlyList authority = null; EDnsHeaderFlags ednsFlags = EDnsHeaderFlags.None; if (dnssecOk) { //DNSSEC enabled foreach (DnsResourceRecord record in answer) { if (record.DnssecStatus == DnssecStatus.Disabled) goto beforeFindClosestNameServers; //dont return answer when status is disabled } //add RRSIG records AddRRSIGRecords(answer, out answer, out authority); ednsFlags = EDnsHeaderFlags.DNSSEC_OK; } IReadOnlyList additional = null; switch (question.Type) { case DnsResourceRecordType.NS: case DnsResourceRecordType.MX: case DnsResourceRecordType.SRV: case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: additional = GetAdditionalRecords(answer, serveStale, dnssecOk, eDnsClientSubnet, advancedForwardingClientSubnet); break; } if (resetExpiry) { foreach (DnsResourceRecord record in answer) { if (record.IsStale) record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767 } if (additional is not null) { foreach (DnsResourceRecord record in additional) { if (record.IsStale) record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767 } } } IReadOnlyList options = null; foreach (DnsResourceRecord record in answer) { if (record.WasExpiryReset || record.IsStale) options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleAnswer, record.Name.ToLowerInvariant() + " " + record.Type.ToString() + " " + record.Class.ToString()))]; } if (eDnsClientSubnet is not null) { EDnsClientSubnetOptionData requestECS = request.GetEDnsClientSubnetOption(true); if (requestECS is not null) { NetworkAddress suitableECS = null; foreach (DnsResourceRecord record in answer) { NetworkAddress recordECS = record.GetCacheRecordInfo().EDnsClientSubnet; if (recordECS is not null) { if ((suitableECS is null) || (recordECS.PrefixLength > suitableECS.PrefixLength)) suitableECS = recordECS; } } if (suitableECS is not null) { EDnsOption[] ecsOption = EDnsClientSubnetOptionData.GetEDnsClientSubnetOption(requestECS.SourcePrefixLength, suitableECS.PrefixLength, requestECS.Address); if (options is null) { options = ecsOption; } else { List newOptions = new List(options.Count + 1); newOptions.AddRange(options); newOptions.Add(ecsOption[0]); options = newOptions; } } } } return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, dnssecOk && (answer.Count > 0) && (answer[0].DnssecStatus == DnssecStatus.Secure), request.CheckingDisabled, DnsResponseCode.NoError, request.Question, answer, authority, additional, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, ednsFlags, options)); } } else { //zone not found //check for DNAME in closest zone if (closest is not null) { IReadOnlyList answer = closest.QueryRecords(DnsResourceRecordType.DNAME, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((answer.Count > 0) && (answer[0].Type == DnsResourceRecordType.DNAME)) { DnsResponseCode rCode; if (DoDNAMESubstitution(question, answer, serveStale, eDnsClientSubnet, advancedForwardingClientSubnet, out answer)) rCode = DnsResponseCode.NoError; else rCode = DnsResponseCode.YXDomain; IReadOnlyList authority = null; EDnsHeaderFlags ednsFlags = EDnsHeaderFlags.None; if (dnssecOk) { //DNSSEC enabled foreach (DnsResourceRecord record in answer) { if (record.DnssecStatus == DnssecStatus.Disabled) goto beforeFindClosestNameServers; //dont return answer when status is disabled } //add RRSIG records AddRRSIGRecords(answer, out answer, out authority); ednsFlags = EDnsHeaderFlags.DNSSEC_OK; } if (resetExpiry) { foreach (DnsResourceRecord record in answer) { if (record.IsStale) record.ResetExpiry(_serveStaleResetTtl); //reset expiry by 30 seconds so that resolver tries again only after 30 seconds as per RFC 8767 } } EDnsOption[] options = null; foreach (DnsResourceRecord record in answer) { if (record.WasExpiryReset || record.IsStale) options = [new EDnsOption(EDnsOptionCode.EXTENDED_DNS_ERROR, new EDnsExtendedDnsErrorOptionData(EDnsExtendedDnsErrorCode.StaleAnswer, record.Name.ToLowerInvariant() + " " + record.Type.ToString() + " " + record.Class.ToString()))]; } return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, dnssecOk && (answer.Count > 0) && (answer[0].DnssecStatus == DnssecStatus.Secure), request.CheckingDisabled, rCode, request.Question, answer, authority, null, request.EDNS is null ? ushort.MinValue : _dnsServer.UdpPayloadSize, ednsFlags, options)); } } } //no answer in cache beforeFindClosestNameServers: //check for closest delegation if any if (findClosestNameServers && (delegation is not null)) { //return closest name servers in delegation if (question.Type == DnsResourceRecordType.DS) { //find parent delegation string domain = AuthZoneManager.GetParentZone(question.Name); if (domain is null) return Task.FromResult(null); //dont find NS for root _ = _root.FindZone(domain, out _, out delegation); if (delegation is null) return Task.FromResult(null); //no cached delegation found } while (true) { IReadOnlyList closestAuthority = delegation.QueryRecords(DnsResourceRecordType.NS, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); if ((closestAuthority.Count == 0) && (delegation.Name.Length == 0)) closestAuthority = delegation.QueryRecords(DnsResourceRecordType.CHILD_NS, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); //root zone case if ((closestAuthority.Count > 0) && (closestAuthority[0].Type == DnsResourceRecordType.NS)) { if (dnssecOk) { if (closestAuthority[0].DnssecStatus != DnssecStatus.Disabled) //dont return records with disabled status { closestAuthority = AddDSRecordsTo(delegation, serveStale, closestAuthority, eDnsClientSubnet, advancedForwardingClientSubnet); IReadOnlyList additional = GetAdditionalRecords(closestAuthority, serveStale, true, eDnsClientSubnet, advancedForwardingClientSubnet); return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, closestAuthority[0].DnssecStatus == DnssecStatus.Secure, request.CheckingDisabled, DnsResponseCode.NoError, request.Question, null, closestAuthority, additional)); } } else { IReadOnlyList additional = GetAdditionalRecords(closestAuthority, serveStale, false, eDnsClientSubnet, advancedForwardingClientSubnet); return Task.FromResult(new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, true, false, request.CheckingDisabled, DnsResponseCode.NoError, request.Question, null, closestAuthority, additional)); } } string domain = AuthZoneManager.GetParentZone(delegation.Name); if (domain is null) return Task.FromResult(null); //dont find NS for root _ = _root.FindZone(domain, out _, out delegation); if (delegation is null) return Task.FromResult(null); //no cached delegation found } } //no cached delegation found return Task.FromResult(null); } #endregion #region properties public uint ServeStaleResetTtl { get { return _serveStaleResetTtl; } set { if ((value < SERVE_STALE_MIN_RESET_TTL) || (value > SERVE_STALE_MAX_RESET_TTL)) throw new ArgumentOutOfRangeException(nameof(ServeStaleResetTtl), "Serve stale reset TTL must be between " + SERVE_STALE_MIN_RESET_TTL + " and " + SERVE_STALE_MAX_RESET_TTL + " seconds. Recommended value is 30 seconds."); _serveStaleResetTtl = value; } } public long MaximumEntries { get { return _maximumEntries; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(MaximumEntries), "Invalid cache maximum entries value. Valid range is 0 and above."); _maximumEntries = value; } } public long TotalEntries { get { return _totalEntries; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/ApexZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { public enum AuthZoneQueryAccess : byte { Deny = 0, Allow = 1, AllowOnlyPrivateNetworks = 2, AllowOnlyZoneNameServers = 3, UseSpecifiedNetworkACL = 4, AllowZoneNameServersAndUseSpecifiedNetworkACL = 5 } public enum AuthZoneTransfer : byte { Deny = 0, Allow = 1, AllowOnlyZoneNameServers = 2, UseSpecifiedNetworkACL = 3, AllowZoneNameServersAndUseSpecifiedNetworkACL = 4 } public enum AuthZoneNotify : byte { None = 0, ZoneNameServers = 1, SpecifiedNameServers = 2, BothZoneAndSpecifiedNameServers = 3, SeparateNameServersForCatalogAndMemberZones = 4 } public enum AuthZoneUpdate : byte { Deny = 0, Allow = 1, AllowOnlyZoneNameServers = 2, UseSpecifiedNetworkACL = 3, AllowZoneNameServersAndUseSpecifiedNetworkACL = 4 } abstract class ApexZone : AuthZone, IDisposable { #region variables protected readonly DnsServer _dnsServer; protected DateTime _lastModified; string _catalogZoneName; bool _overrideCatalogQueryAccess; bool _overrideCatalogZoneTransfer; bool _overrideCatalogNotify; protected AuthZoneQueryAccess _queryAccess; IReadOnlyCollection _queryAccessNetworkACL; protected AuthZoneTransfer _zoneTransfer; IReadOnlyCollection _zoneTransferNetworkACL; IReadOnlySet _zoneTransferTsigKeyNames; readonly List _zoneHistory; //for IXFR support AuthZoneNotify _notify; IReadOnlyCollection _notifyNameServers; IReadOnlyCollection _notifySecondaryCatalogNameServers; AuthZoneUpdate _update; IReadOnlyCollection _updateNetworkACL; IReadOnlyDictionary>> _updateSecurityPolicies; protected AuthZoneDnssecStatus _dnssecStatus; Timer _notifyTimer; bool _notifyTimerTriggered; const int NOTIFY_TIMER_INTERVAL = 5000; List _notifyList; List _notifyFailed; const int NOTIFY_TIMEOUT = 10000; const int NOTIFY_RETRIES = 5; protected bool _syncFailed; Timer _recordExpiryTimer; readonly object _recordExpiryTimerLock = new object(); DateTime _recordExpiryTimerStartedOn; uint _recordExpiryTimerTtl; bool _recordExpiryTimerRunning; CatalogZone _catalogZone; SecondaryCatalogZone _secondaryCatalogZone; #endregion #region constructor protected ApexZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(zoneInfo) { _dnsServer = dnsServer; _catalogZoneName = zoneInfo.CatalogZoneName; _overrideCatalogQueryAccess = zoneInfo.OverrideCatalogQueryAccess; _overrideCatalogZoneTransfer = zoneInfo.OverrideCatalogZoneTransfer; _overrideCatalogNotify = zoneInfo.OverrideCatalogNotify; _queryAccess = zoneInfo.QueryAccess; _queryAccessNetworkACL = zoneInfo.QueryAccessNetworkACL; _zoneTransfer = zoneInfo.ZoneTransfer; _zoneTransferNetworkACL = zoneInfo.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = zoneInfo.ZoneTransferTsigKeyNames; if (zoneInfo.ZoneHistory is null) _zoneHistory = new List(); else _zoneHistory = new List(zoneInfo.ZoneHistory); _notify = zoneInfo.Notify; _notifyNameServers = zoneInfo.NotifyNameServers; _notifySecondaryCatalogNameServers = zoneInfo.NotifySecondaryCatalogNameServers; _update = zoneInfo.Update; _updateNetworkACL = zoneInfo.UpdateNetworkACL; _updateSecurityPolicies = zoneInfo.UpdateSecurityPolicies; _lastModified = zoneInfo.LastModified; } protected ApexZone(DnsServer dnsServer, string name) : base(name) { _dnsServer = dnsServer; _queryAccess = AuthZoneQueryAccess.Allow; _zoneHistory = new List(); _lastModified = DateTime.UtcNow; } #endregion #region IDisposable bool _disposed; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _notifyTimer?.Dispose(); lock (_recordExpiryTimerLock) { if (_recordExpiryTimer is not null) { _recordExpiryTimer.Dispose(); _recordExpiryTimer = null; } } } _disposed = true; } public void Dispose() { Dispose(true); } #endregion #region notify protected void InitNotify() { _notifyTimer = new Timer(NotifyTimerCallback, null, Timeout.Infinite, Timeout.Infinite); _notifyList = new List(); _notifyFailed = new List(); } protected void DisableNotifyTimer() { if (_notifyTimer is not null) _notifyTimer.Change(Timeout.Infinite, Timeout.Infinite); } private void NotifyTimerCallback(object state) { ApexZone apexZone = this; if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogNotify) apexZone = apexZone.CatalogZone; List notifiedNameServers = new List(); async Task NotifyZoneNameServersAsync(bool onlyFailedNameServers) { string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer; IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords //notify all secondary name servers List tasks = new List(); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; string nameServerHost = (nsRecord.RDATA as DnsNSRecordData).NameServer; if (primaryNameServer.Equals(nameServerHost, StringComparison.OrdinalIgnoreCase)) continue; //skip primary name server if (onlyFailedNameServers) { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) continue; } } notifiedNameServers.Add(nameServerHost); List nameServers = new List(2); await ResolveNameServerAddressesAsync(nsRecord, nameServers); if (nameServers.Count > 0) { tasks.Add(NotifyNameServerAsync(nameServerHost, nameServers)); } else { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) _notifyFailed.Add(nameServerHost); } _dnsServer.LogManager.Write("DNS Server failed to notify name server '" + nameServerHost + "' due to failure in resolving its IP address for zone: " + ToString()); } } await Task.WhenAll(tasks); } Task NotifySpecifiedNameServersAsync(bool onlyFailedNameServers) { IReadOnlyCollection specifiedNameServers = apexZone._notifyNameServers; if (specifiedNameServers is not null) return NotifyNameServersAsync(specifiedNameServers, onlyFailedNameServers); return Task.CompletedTask; } Task NotifySecondaryCatalogNameServersAsync(bool onlyFailedNameServers) { IReadOnlyCollection secondaryCatalogNameServers = apexZone._notifySecondaryCatalogNameServers; if (secondaryCatalogNameServers is not null) return NotifyNameServersAsync(secondaryCatalogNameServers, onlyFailedNameServers); return Task.CompletedTask; } async Task NotifyNameServersAsync(IReadOnlyCollection nameServerIpAddresses, bool onlyFailedNameServers) { List tasks = new List(); foreach (IPAddress nameServerIpAddress in nameServerIpAddresses) { string nameServerHost = nameServerIpAddress.ToString(); if (onlyFailedNameServers) { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) continue; } } notifiedNameServers.Add(nameServerHost); tasks.Add(NotifyNameServerAsync(nameServerHost, [new NameServerAddress(nameServerIpAddress)])); } await Task.WhenAll(tasks); } //notify in DNS server's resolver thread pool if (!_dnsServer.TryQueueResolverTask(async delegate (object state) { try { switch (apexZone._notify) { case AuthZoneNotify.ZoneNameServers: await NotifyZoneNameServersAsync(!_notifyTimerTriggered); break; case AuthZoneNotify.SpecifiedNameServers: await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered); break; case AuthZoneNotify.BothZoneAndSpecifiedNameServers: Task t1 = NotifyZoneNameServersAsync(!_notifyTimerTriggered); Task t2 = NotifySpecifiedNameServersAsync(!_notifyTimerTriggered); await Task.WhenAll(t1, t2); break; case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones: if (this is CatalogZone) await NotifySecondaryCatalogNameServersAsync(!_notifyTimerTriggered); else await NotifySpecifiedNameServersAsync(!_notifyTimerTriggered); break; } //remove non-existent name servers from notify failed list lock (_notifyFailed) { if (_notifyFailed.Count > 0) { List toRemove = new List(); foreach (string failedNameServer in _notifyFailed) { if (!notifiedNameServers.Contains(failedNameServer)) toRemove.Add(failedNameServer); } foreach (string failedNameServer in toRemove) _notifyFailed.Remove(failedNameServer); if (_notifyFailed.Count > 0) { //set timer to notify failed name servers again _notifyTimer?.Change(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000, Timeout.Infinite); } } } } catch (ObjectDisposedException) { } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { _notifyTimerTriggered = false; } }) ) { //failed to queue notify task; try again in some time try { _notifyTimer?.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite); } catch (ObjectDisposedException) { } } } private async Task NotifyNameServerAsync(string nameServerHost, IReadOnlyList nameServers) { //use notify list to prevent multiple threads from notifying the same name server lock (_notifyList) { if (_notifyList.Contains(nameServerHost)) return; //already notifying the name server in another thread _notifyList.Add(nameServerHost); } try { DnsClient client = new DnsClient(nameServers); client.Proxy = _dnsServer.Proxy; client.Timeout = NOTIFY_TIMEOUT; client.Retries = NOTIFY_RETRIES; DnsDatagram notifyRequest = new DnsDatagram(0, false, DnsOpcode.Notify, true, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN) }, _entries[DnsResourceRecordType.SOA]); DnsDatagram response = await client.RawResolveAsync(notifyRequest); switch (response.RCODE) { case DnsResponseCode.NoError: case DnsResponseCode.NotImplemented: { //transaction complete lock (_notifyFailed) { _notifyFailed.Remove(nameServerHost); } _dnsServer.LogManager.Write("DNS Server successfully notified name server '" + nameServerHost + "' for zone: " + ToString()); } break; default: { //transaction failed lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) _notifyFailed.Add(nameServerHost); } _dnsServer.LogManager.Write("DNS Server failed to notify name server '" + nameServerHost + "' (RCODE=" + response.RCODE.ToString() + ") for zone: " + ToString()); } break; } } catch (Exception ex) { lock (_notifyFailed) { if (!_notifyFailed.Contains(nameServerHost)) _notifyFailed.Add(nameServerHost); } _dnsServer.LogManager.Write("DNS Server failed to notify name server '" + nameServerHost + "' for zone: " + ToString() + "\r\n" + ex.ToString()); } finally { lock (_notifyList) { _notifyList.Remove(nameServerHost); } } } internal void RemoveFromNotifyFailedList(NameServerAddress allowedZoneNameServer, IPAddress allowedIPAddress) { if (_notifyFailed is null) return; lock (_notifyFailed) { if (_notifyFailed.Count == 0) return; if ((allowedZoneNameServer is not null) && (allowedZoneNameServer.DomainEndPoint is not null)) _notifyFailed.Remove(allowedZoneNameServer.DomainEndPoint.Address); _notifyFailed.Remove(allowedIPAddress.ToString()); } } public void TriggerNotify() { if (Disabled) return; ApexZone apexZone = this; if ((apexZone.CatalogZone is not null) && !apexZone.OverrideCatalogNotify) apexZone = apexZone.CatalogZone; if (apexZone._notify == AuthZoneNotify.None) { if (_notifyFailed is not null) { lock (_notifyFailed) { _notifyFailed.Clear(); } } return; } if (_notifyTimerTriggered) return; if (_disposed) return; if (_notifyTimer is null) return; _notifyTimer.Change(NOTIFY_TIMER_INTERVAL, Timeout.Infinite); _notifyTimerTriggered = true; } #endregion #region record expiry protected void InitRecordExpiry() { _recordExpiryTimer = new Timer(RecordExpiryTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } private uint GetMinRecordExpiryTtl(uint minExpiryTtl) { if (!_recordExpiryTimerRunning) return Math.Min(minExpiryTtl, uint.MaxValue / 1000); uint elapsedSeconds = Convert.ToUInt32((DateTime.UtcNow - _recordExpiryTimerStartedOn).TotalSeconds); if (elapsedSeconds >= _recordExpiryTimerTtl) return 0u; uint pendingExpiryTtl = _recordExpiryTimerTtl - elapsedSeconds; return Math.Min(Math.Min(pendingExpiryTtl, minExpiryTtl), uint.MaxValue / 1000); } public void StartRecordExpiryTimer(uint minExpiryTtl) { lock (_recordExpiryTimerLock) { if (_recordExpiryTimer is not null) { uint minTtl = GetMinRecordExpiryTtl(minExpiryTtl); _recordExpiryTimer.Change(minTtl * 1000, Timeout.Infinite); _recordExpiryTimerStartedOn = DateTime.UtcNow; _recordExpiryTimerTtl = minTtl; _recordExpiryTimerRunning = true; } } } private void RecordExpiryTimerCallback(object state) { _recordExpiryTimerRunning = false; uint minExpiryTtl = 0u; try { IReadOnlyList authZones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); bool recordsDeleted = false; foreach (AuthZone authZone in authZones) { foreach (KeyValuePair> entry in authZone.Entries) { foreach (DnsResourceRecord record in entry.Value) { GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); if (recordInfo.ExpiryTtl > 0u) { uint pendingExpiryTtl = recordInfo.GetPendingExpiryTtl(); if (pendingExpiryTtl == 0u) { if (_dnsServer.AuthZoneManager.DeleteRecord(_name, record)) recordsDeleted = true; } else { if (minExpiryTtl == 0u) minExpiryTtl = pendingExpiryTtl; else minExpiryTtl = Math.Min(minExpiryTtl, pendingExpiryTtl); } } } } } if (recordsDeleted) _dnsServer.AuthZoneManager.SaveZoneFile(_name); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { if (minExpiryTtl > 0u) StartRecordExpiryTimer(minExpiryTtl); } } #endregion #region internal internal virtual void UpdateDnssecStatus() { if (!_entries.ContainsKey(DnsResourceRecordType.DNSKEY)) _dnssecStatus = AuthZoneDnssecStatus.Unsigned; else if (_entries.ContainsKey(DnsResourceRecordType.NSEC3PARAM)) _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3; else _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC; } #endregion #region versioning internal virtual void CommitAndIncrementSerial(IReadOnlyList deletedRecords = null, IReadOnlyList addedRecords = null) { _lastModified = DateTime.UtcNow; if (addedRecords is not null) { uint minExpiryTtl = 0u; foreach (DnsResourceRecord addedRecord in addedRecords) { uint expiryTtl = addedRecord.GetAuthGenericRecordInfo().ExpiryTtl; if (expiryTtl > 0u) { if (minExpiryTtl == 0u) minExpiryTtl = expiryTtl; else minExpiryTtl = Math.Min(minExpiryTtl, expiryTtl); } } if (minExpiryTtl > 0u) StartRecordExpiryTimer(minExpiryTtl); } lock (_zoneHistory) { DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsResourceRecord newSoaRecord; { DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData; if ((addedRecords is not null) && (addedRecords.Count == 1) && (addedRecords[0].Type == DnsResourceRecordType.SOA)) { DnsResourceRecord addSoaRecord = addedRecords[0]; DnsSOARecordData addSoa = addSoaRecord.RDATA as DnsSOARecordData; uint serial = GetNewSerial(oldSoa.Serial, addSoa.Serial, addSoaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme); newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, addSoaRecord.TTL, new DnsSOARecordData(addSoa.PrimaryNameServer, addSoa.ResponsiblePerson, serial, addSoa.Refresh, addSoa.Retry, addSoa.Expire, addSoa.Minimum)) { Tag = addSoaRecord.Tag }; addedRecords = null; } else { uint serial = GetNewSerial(oldSoa.Serial, 0, oldSoaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme); newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TTL, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, serial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag }; } } DnsResourceRecord[] newSoaRecords = [newSoaRecord]; //update SOA _entries[DnsResourceRecordType.SOA] = newSoaRecords; IReadOnlyList newRRSigRecords = null; IReadOnlyList deletedRRSigRecords = null; if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) { //sign SOA and update RRSig newRRSigRecords = SignRRSet(newSoaRecords); AddOrUpdateRRSigRecords(newRRSigRecords, out deletedRRSigRecords); } //remove RR info from old SOA to allow creating new history RR info for setting DeletedOn oldSoaRecord.Tag = null; //start commit oldSoaRecord.GetAuthHistoryRecordInfo().DeletedOn = DateTime.UtcNow; //write removed _zoneHistory.Add(oldSoaRecord); if (deletedRecords is not null) { foreach (DnsResourceRecord deletedRecord in deletedRecords) { if (deletedRecord.GetAuthGenericRecordInfo().Disabled) continue; _zoneHistory.Add(deletedRecord); if (deletedRecord.Type == DnsResourceRecordType.NS) { IReadOnlyList glueRecords = deletedRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) _zoneHistory.AddRange(glueRecords); } } } if (deletedRRSigRecords is not null) _zoneHistory.AddRange(deletedRRSigRecords); //write added _zoneHistory.Add(newSoaRecord); if (addedRecords is not null) { foreach (DnsResourceRecord addedRecord in addedRecords) { if (addedRecord.GetAuthGenericRecordInfo().Disabled) continue; _zoneHistory.Add(addedRecord); if (addedRecord.Type == DnsResourceRecordType.NS) { IReadOnlyList glueRecords = addedRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) _zoneHistory.AddRange(glueRecords); } } } if (newRRSigRecords is not null) _zoneHistory.AddRange(newRRSigRecords); //end commit CleanupHistory(); } } protected static uint GetNewSerial(uint oldSerial, uint updateSerial, bool useSoaSerialDateScheme) { if (useSoaSerialDateScheme) { string strOldSerial = oldSerial.ToString(); string strOldSerialDate = null; byte counter = 0; if (strOldSerial.Length == 10) { //parse old serial strOldSerialDate = strOldSerial.Substring(0, 8); counter = byte.Parse(strOldSerial.Substring(8)); } string strSerialDate = DateTime.UtcNow.ToString("yyyyMMdd"); if (strOldSerialDate is null) { //transitioning to date scheme return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')); } else if (strSerialDate.Equals(strOldSerialDate)) { //same date if (counter < 99) { counter++; return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')); } else { //more than 100 increments return uint.Parse(strSerialDate + counter.ToString().PadLeft(2, '0')) + 1; } } else if (uint.Parse(strSerialDate) > uint.Parse(strOldSerialDate)) { //later date return uint.Parse(strSerialDate + "00"); } } //default uint serial = oldSerial; if (updateSerial > serial) serial = updateSerial; else if (serial < uint.MaxValue) serial++; else serial = 1; return serial; } internal void SetSoaSerial(uint newSerial) { lock (_zoneHistory) { DnsResourceRecord oldSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsSOARecordData oldSoa = oldSoaRecord.RDATA as DnsSOARecordData; DnsResourceRecord newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, oldSoaRecord.TTL, new DnsSOARecordData(oldSoa.PrimaryNameServer, oldSoa.ResponsiblePerson, newSerial, oldSoa.Refresh, oldSoa.Retry, oldSoa.Expire, oldSoa.Minimum)) { Tag = oldSoaRecord.Tag }; DnsResourceRecord[] newSoaRecords = [newSoaRecord]; //update SOA _entries[DnsResourceRecordType.SOA] = newSoaRecords; //clear history _zoneHistory.Clear(); } } public IReadOnlyList GetZoneHistory() { lock (_zoneHistory) { return _zoneHistory.ToArray(); } } protected void CleanupHistory() { DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData; DateTime expiry = DateTime.UtcNow.AddSeconds(-soa.Expire); int index = 0; while (index < _zoneHistory.Count) { //check difference sequence if (_zoneHistory[index].GetAuthHistoryRecordInfo().DeletedOn > expiry) break; //found record to keep //skip to next difference sequence index++; int soaCount = 1; while (index < _zoneHistory.Count) { if (_zoneHistory[index].Type == DnsResourceRecordType.SOA) { soaCount++; if (soaCount == 3) break; } index++; } } if (index == _zoneHistory.Count) { //delete entire history _zoneHistory.Clear(); return; } //remove expired records _zoneHistory.RemoveRange(0, index); } protected void CommitZoneHistory(IReadOnlyList historyRecords) { lock (_zoneHistory) { historyRecords[0].GetAuthHistoryRecordInfo().DeletedOn = DateTime.UtcNow; //write history _zoneHistory.AddRange(historyRecords); CleanupHistory(); } } protected void ClearZoneHistory() { lock (_zoneHistory) { _zoneHistory.Clear(); } } #endregion #region catalog zone private IReadOnlyCollection GetQueryAccessACL() { switch (_queryAccess) { case AuthZoneQueryAccess.Allow: return [ new NetworkAccessControl(IPAddress.Any, 0), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; case AuthZoneQueryAccess.AllowOnlyPrivateNetworks: return [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("10.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("100.64.0.0"), 10), new NetworkAccessControl(IPAddress.Parse("169.254.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("172.16.0.0"), 12), new NetworkAccessControl(IPAddress.Parse("192.168.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("2000::"), 3, true), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; case AuthZoneQueryAccess.AllowOnlyZoneNameServers: return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; case AuthZoneQueryAccess.UseSpecifiedNetworkACL: return _queryAccessNetworkACL; case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: if (_queryAccessNetworkACL is null) { return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; } return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32), .._queryAccessNetworkACL ]; case AuthZoneQueryAccess.Deny: default: return [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("::1"), 128) ]; } } private IReadOnlyCollection GetZoneTranferACL() { switch (_zoneTransfer) { case AuthZoneTransfer.Allow: return [ new NetworkAccessControl(IPAddress.Any, 0), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; case AuthZoneTransfer.AllowOnlyZoneNameServers: return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; case AuthZoneTransfer.UseSpecifiedNetworkACL: return _zoneTransferNetworkACL; case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: if (_zoneTransferNetworkACL is null) { return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; } return [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32), .._zoneTransferNetworkACL ]; case AuthZoneTransfer.Deny: default: return [ new NetworkAccessControl(IPAddress.Any, 0, true), new NetworkAccessControl(IPAddress.IPv6Any, 0, true) ]; } } #endregion #region public public uint GetZoneSoaSerial() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Serial; } public uint GetZoneSoaRetry() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Retry; } public uint GetZoneSoaExpire() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Expire; } public uint GetZoneSoaMinimum() { return (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).Minimum; } public abstract string GetZoneTypeName(); public override string ToString() { return _name.Length == 0 ? "" : _name; } #endregion #region name server address resolution public async Task> GetResolvedPrimaryNameServerAddressesAsync() { IReadOnlyList primaryNameServers; if (this is SecondaryZone secondary) primaryNameServers = secondary.PrimaryNameServerAddresses; else if (this is StubZone stub) primaryNameServers = stub.PrimaryNameServerAddresses; else primaryNameServers = null; if (primaryNameServers is not null) return await GetResolvedNameServerAddressesAsync(primaryNameServers); DnsResourceRecord soaRecord = _entries[DnsResourceRecordType.SOA][0]; string primaryNameServer = (soaRecord.RDATA as DnsSOARecordData).PrimaryNameServer; IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords List nameServers = new List(nsRecords.Count * 2); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase)) { //found primary NS await ResolveNameServerAddressesAsync(nsRecord, nameServers); break; } } if (nameServers.Count < 1) await ResolveNameServerAddressesAsync(primaryNameServer, 53, DnsTransportProtocol.Udp, nameServers); return nameServers; } public async Task> GetResolvedSecondaryNameServerAddressesAsync() { string primaryNameServer = (_entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData).PrimaryNameServer; IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords List nameServers = new List(nsRecords.Count * 2); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; if (primaryNameServer.Equals((nsRecord.RDATA as DnsNSRecordData).NameServer, StringComparison.OrdinalIgnoreCase)) continue; //skip primary name server await ResolveNameServerAddressesAsync(nsRecord, nameServers); } return nameServers; } public async Task> GetAllResolvedNameServerAddressesAsync() { IReadOnlyList nsRecords = GetRecords(DnsResourceRecordType.NS); //stub zone has no authority so cant use QueryRecords List nameServers = new List(nsRecords.Count * 2); foreach (DnsResourceRecord nsRecord in nsRecords) { if (nsRecord.GetAuthGenericRecordInfo().Disabled) continue; await ResolveNameServerAddressesAsync(nsRecord, nameServers); } return nameServers; } public async Task> GetResolvedNameServerAddressesAsync(IReadOnlyList nameServers) { List resolvedNameServers = new List(nameServers.Count * 2); List resolverTasks = new List(nameServers.Count); foreach (NameServerAddress nameServer in nameServers) { if (nameServer.IsIPEndPointStale) resolverTasks.Add(ResolveNameServerAddressesAsync(nameServer.Host, nameServer.Port, nameServer.Protocol, resolvedNameServers)); else resolvedNameServers.Add(nameServer); } await Task.WhenAll(resolverTasks); return resolvedNameServers; } private async Task ResolveNameServerAddressesAsync(string nsDomain, int port, DnsTransportProtocol protocol, List outNameServers, CancellationToken cancellationToken = default) { try { DnsDatagram response = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.A, DnsClass.IN), cancellationToken: cancellationToken); if (response.Answer.Count > 0) { IReadOnlyList addresses = DnsClient.ParseResponseA(response); foreach (IPAddress address in addresses) outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol)); } } catch (Exception ex) { _dnsServer.ResolverLogManager?.Write(ex); } if (_dnsServer.PreferIPv6) { try { DnsDatagram response = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(nsDomain, DnsResourceRecordType.AAAA, DnsClass.IN), cancellationToken: cancellationToken); if (response.Answer.Count > 0) { IReadOnlyList addresses = DnsClient.ParseResponseAAAA(response); foreach (IPAddress address in addresses) outNameServers.Add(new NameServerAddress(nsDomain, new IPEndPoint(address, port), protocol)); } } catch (Exception ex) { _dnsServer.ResolverLogManager?.Write(ex); } } } private Task ResolveNameServerAddressesAsync(DnsResourceRecord nsRecord, List outNameServers) { string nsDomain = (nsRecord.RDATA as DnsNSRecordData).NameServer; IReadOnlyList glueRecords = nsRecord.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) { foreach (DnsResourceRecord glueRecord in glueRecords) { switch (glueRecord.Type) { case DnsResourceRecordType.A: outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsARecordData).Address)); break; case DnsResourceRecordType.AAAA: if (_dnsServer.PreferIPv6) outNameServers.Add(new NameServerAddress(nsDomain, (glueRecord.RDATA as DnsAAAARecordData).Address)); break; } } return Task.CompletedTask; } else { return ResolveNameServerAddressesAsync(nsDomain, 53, DnsTransportProtocol.Udp, outNameServers); } } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { if (base.Disabled == value) return; base.Disabled = value; //set value early to be able to use it for setting catalog properties CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (value) _dnsServer.AuthZoneManager.RemoveCatalogMemberZone(new AuthZoneInfo(this), true); //remove catalog zone membership without removing it from zone's options else _dnsServer.AuthZoneManager.AddCatalogMemberZone(_catalogZoneName, new AuthZoneInfo(this), true); //add catalog zone membership } } } public DateTime LastModified { get { return _lastModified; } } public virtual string CatalogZoneName { get { return _catalogZoneName; } set { if (string.IsNullOrEmpty(value)) _catalogZoneName = null; else _catalogZoneName = value; //reset _catalogZone = null; _secondaryCatalogZone = null; } } public virtual bool OverrideCatalogQueryAccess { get { return _overrideCatalogQueryAccess; } set { _overrideCatalogQueryAccess = value; } } public virtual bool OverrideCatalogZoneTransfer { get { return _overrideCatalogZoneTransfer; } set { _overrideCatalogZoneTransfer = value; } } public virtual bool OverrideCatalogNotify { get { return _overrideCatalogNotify; } set { _overrideCatalogNotify = value; } } public virtual AuthZoneQueryAccess QueryAccess { get { return _queryAccess; } set { _queryAccess = value; //update catalog zone property if (this is CatalogZone thisCatalogZone) { //update global custom property thisCatalogZone.SetAllowQueryProperty(GetQueryAccessACL()); } else if (!Disabled && ((this is PrimaryZone) || (this is SecondaryZone && this is not SecondaryForwarderZone) || (this is StubZone) || (this is ForwarderZone))) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_overrideCatalogQueryAccess) catalogZone.SetAllowQueryProperty(GetQueryAccessACL(), _name); //update member zone custom property else catalogZone.SetAllowQueryProperty(null, _name); //remove member zone custom property } } } } public IReadOnlyCollection QueryAccessNetworkACL { get { return _queryAccessNetworkACL; } set { if ((value is null) || (value.Count == 0)) _queryAccessNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(QueryAccessNetworkACL), "Network ACL cannot have more than 255 entries."); else _queryAccessNetworkACL = value; } } public virtual AuthZoneTransfer ZoneTransfer { get { return _zoneTransfer; } set { _zoneTransfer = value; //update catalog zone property if (this is CatalogZone thisCatalogZone) { //update global custom property thisCatalogZone.SetAllowTransferProperty(GetZoneTranferACL()); } else if (!Disabled && ((this is PrimaryZone) || (this is SecondaryZone && this is not SecondaryForwarderZone))) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_overrideCatalogZoneTransfer) catalogZone.SetAllowTransferProperty(GetZoneTranferACL(), _name); //update member zone custom property else catalogZone.SetAllowTransferProperty(null, _name); //remove member zone custom property } } } } public IReadOnlyCollection ZoneTransferNetworkACL { get { return _zoneTransferNetworkACL; } set { if ((value is null) || (value.Count == 0)) _zoneTransferNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ZoneTransferNetworkACL), "Network ACL cannot have more than 255 entries."); else _zoneTransferNetworkACL = value; } } public IReadOnlySet ZoneTransferTsigKeyNames { get { return _zoneTransferTsigKeyNames; } set { if ((value is null) || (value.Count == 0)) _zoneTransferTsigKeyNames = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(ZoneTransferTsigKeyNames), "Zone transfer TSIG key names cannot have more than 255 entries."); else _zoneTransferTsigKeyNames = value; //update catalog zone property if (this is CatalogZone thisCatalogZone) { //update global custom property thisCatalogZone.SetZoneTransferTsigKeyNamesProperty(_zoneTransferTsigKeyNames); } else if (!Disabled && ((this is PrimaryZone) || (this is SecondaryZone && this is not SecondaryForwarderZone))) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_overrideCatalogZoneTransfer) catalogZone.SetZoneTransferTsigKeyNamesProperty(_zoneTransferTsigKeyNames, _name); //update member zone custom property else catalogZone.SetZoneTransferTsigKeyNamesProperty(null, _name); //remove member zone custom property } } } } public virtual AuthZoneNotify Notify { get { return _notify; } set { _notify = value; lock (_notifyFailed) { _notifyFailed.Clear(); } } } public IReadOnlyCollection NotifyNameServers { get { return _notifyNameServers; } set { if ((value is null) || (value.Count == 0)) _notifyNameServers = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(NotifyNameServers), "Name server addresses cannot have more than 255 entries."); else _notifyNameServers = value; lock (_notifyFailed) { _notifyFailed.Clear(); } } } public IReadOnlyCollection NotifySecondaryCatalogNameServers { get { return _notifySecondaryCatalogNameServers; } set { if ((value is null) || (value.Count == 0)) _notifySecondaryCatalogNameServers = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(NotifySecondaryCatalogNameServers), "Secondary Catalog name server addresses cannot have more than 255 entries."); else _notifySecondaryCatalogNameServers = value; lock (_notifyFailed) { _notifyFailed.Clear(); } } } public virtual AuthZoneUpdate Update { get { return _update; } set { _update = value; } } public IReadOnlyCollection UpdateNetworkACL { get { return _updateNetworkACL; } set { if ((value is null) || (value.Count == 0)) _updateNetworkACL = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(UpdateNetworkACL), "Network ACL cannot have more than 255 entries."); else _updateNetworkACL = value; } } public IReadOnlyDictionary>> UpdateSecurityPolicies { get { return _updateSecurityPolicies; } set { _updateSecurityPolicies = value; } } public AuthZoneDnssecStatus DnssecStatus { get { return _dnssecStatus; } } public string[] NotifyFailed { get { if (_notifyFailed is null) return Array.Empty(); lock (_notifyFailed) { if (_notifyFailed.Count > 0) return _notifyFailed.ToArray(); return Array.Empty(); } } } public bool SyncFailed { get { return _syncFailed; } } public CatalogZone CatalogZone { get { if (_catalogZoneName is null) return null; if (_secondaryCatalogZone is not null) return null; if (_catalogZone is null) { if ((this is PrimaryZone) || (this is ForwarderZone)) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is CatalogZone catalogZone) _catalogZone = catalogZone; } else if ((this is StubZone) || (this is SecondaryZone && this is not SecondaryForwarderZone)) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is CatalogZone catalogZone) _catalogZone = catalogZone; else if (apexZone is SecondaryCatalogZone secondaryCatalogZone) _secondaryCatalogZone = secondaryCatalogZone; } } return _catalogZone; } } public SecondaryCatalogZone SecondaryCatalogZone { get { if (_catalogZoneName is null) return null; if (_catalogZone is not null) return null; if (_secondaryCatalogZone is null) { if (this is SecondaryZone) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is SecondaryCatalogZone secondaryCatalogZone) _secondaryCatalogZone = secondaryCatalogZone; } else if (this is StubZone) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(_catalogZoneName); if (apexZone is SecondaryCatalogZone secondaryCatalogZone) _secondaryCatalogZone = secondaryCatalogZone; else if (apexZone is CatalogZone catalogZone) _catalogZone = catalogZone; } } return _secondaryCatalogZone; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/AuthZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { abstract class AuthZone : Zone { #region variables bool _disabled; #endregion #region constructor protected AuthZone(AuthZoneInfo zoneInfo) : base(zoneInfo.Name) { _disabled = zoneInfo.Disabled; } protected AuthZone(string name) : base(name) { } #endregion #region private private IReadOnlyList FilterDisabledRecords(DnsResourceRecordType type, IReadOnlyList records) { if (_disabled) return Array.Empty(); if (records.Count == 1) { GenericRecordInfo authRecordInfo = records[0].GetAuthGenericRecordInfo(); if (authRecordInfo.Disabled) return Array.Empty(); //record disabled //update last used on authRecordInfo.LastUsedOn = DateTime.UtcNow; return records; } List newRecords = new List(records.Count); DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in records) { GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo(); if (authRecordInfo.Disabled) continue; //record disabled //update last used on authRecordInfo.LastUsedOn = utcNow; newRecords.Add(record); } if (newRecords.Count > 1) { switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: case DnsResourceRecordType.NS: newRecords.Shuffle(); //shuffle records to allow load balancing break; } } return newRecords; } private IReadOnlyList AppendRRSigTo(IReadOnlyList records) { IReadOnlyList rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG); if (rrsigRecords.Count == 0) return records; DnsResourceRecordType type = records[0].Type; List newRecords = new List(records.Count + 2); newRecords.AddRange(records); DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord rrsigRecord in rrsigRecords) { if ((rrsigRecord.RDATA as DnsRRSIGRecordData).TypeCovered == type) { rrsigRecord.GetAuthGenericRecordInfo().LastUsedOn = utcNow; newRecords.Add(rrsigRecord); } } return newRecords; } #endregion #region versioning internal bool TrySetRecords(DnsResourceRecordType type, IReadOnlyList records, out IReadOnlyList deletedRecords) { switch (type) { case DnsResourceRecordType.CNAME: if ((!_entries.IsEmpty) && !_entries.ContainsKey(DnsResourceRecordType.CNAME)) throw new InvalidOperationException("Cannot add record: a CNAME record cannot exists with other record types for the same name."); break; case DnsResourceRecordType.NSEC: case DnsResourceRecordType.RRSIG: break; //ignore default: if (_entries.ContainsKey(DnsResourceRecordType.CNAME)) throw new InvalidOperationException("Cannot add record: a CNAME record cannot exists with other record types for the same name."); break; } if (_entries.TryGetValue(type, out IReadOnlyList existingRecords)) { deletedRecords = existingRecords; return _entries.TryUpdate(type, records, existingRecords); } else { deletedRecords = Array.Empty(); return _entries.TryAdd(type, records); } } internal bool TryDeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata, out DnsResourceRecord deletedRecord) { if (_entries.TryGetValue(type, out IReadOnlyList existingRecords)) { if (existingRecords.Count == 1) { if (rdata.Equals(existingRecords[0].RDATA)) { if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) { deletedRecord = removedRecords[0]; return true; } } } else { deletedRecord = null; List updatedRecords = new List(existingRecords.Count); foreach (DnsResourceRecord existingRecord in existingRecords) { if ((deletedRecord is null) && rdata.Equals(existingRecord.RDATA)) deletedRecord = existingRecord; else updatedRecords.Add(existingRecord); } if (deletedRecord is null) return false; //not found return _entries.TryUpdate(type, updatedRecords, existingRecords); } } deletedRecord = null; return false; } internal bool TryDeleteRecords(DnsResourceRecordType type, IReadOnlyList records, out IReadOnlyList deletedRecords) { if (_entries.TryGetValue(type, out IReadOnlyList existingRecords)) { if (existingRecords.Count == 1) { DnsResourceRecord existingRecord = existingRecords[0]; foreach (DnsResourceRecord record in records) { if (record.RDATA.Equals(existingRecord.RDATA)) { if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) { deletedRecords = removedRecords; return true; } } } } else { List deleted = new List(records.Count); List updatedRecords = new List(existingRecords.Count); foreach (DnsResourceRecord existingRecord in existingRecords) { bool found = false; foreach (DnsResourceRecord record in records) { if (record.RDATA.Equals(existingRecord.RDATA)) { found = true; break; } } if (found) deleted.Add(existingRecord); else updatedRecords.Add(existingRecord); } if (deleted.Count > 0) { deletedRecords = deleted; if (updatedRecords.Count > 0) return _entries.TryUpdate(type, updatedRecords, existingRecords); return _entries.TryRemove(type, out _); } } } deletedRecords = null; return false; } internal void AddOrUpdateRRSigRecords(IReadOnlyList newRRSigRecords, out IReadOnlyList deletedRRSigRecords) { IReadOnlyList deleted = null; _entries.AddOrUpdate(DnsResourceRecordType.RRSIG, delegate (DnsResourceRecordType key) { deleted = Array.Empty(); return newRRSigRecords; }, delegate (DnsResourceRecordType key, IReadOnlyList existingRecords) { List updatedRecords = new List(existingRecords.Count + newRRSigRecords.Count); List deletedRecords = new List(); foreach (DnsResourceRecord existingRecord in existingRecords) { bool found = false; DnsRRSIGRecordData existingRRSig = existingRecord.RDATA as DnsRRSIGRecordData; foreach (DnsResourceRecord newRRSigRecord in newRRSigRecords) { DnsRRSIGRecordData newRRSig = newRRSigRecord.RDATA as DnsRRSIGRecordData; if ((newRRSig.TypeCovered == existingRRSig.TypeCovered) && (newRRSig.KeyTag == existingRRSig.KeyTag)) { deletedRecords.Add(existingRecord); found = true; break; } } if (!found) updatedRecords.Add(existingRecord); } updatedRecords.AddRange(newRRSigRecords); deleted = deletedRecords; return updatedRecords; }); deletedRRSigRecords = deleted; } internal void AddRecord(DnsResourceRecord record, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords) { switch (record.Type) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot add record: use SetRecords() for " + record.Type.ToString() + " record."); default: if (_entries.ContainsKey(DnsResourceRecordType.CNAME)) throw new InvalidOperationException("Cannot add record: a CNAME record cannot exists with other record types for the same name."); break; } List added = new List(); List deleted = new List(); addedRecords = added; deletedRecords = deleted; _entries.AddOrUpdate(record.Type, delegate (DnsResourceRecordType key) { added.Add(record); return new DnsResourceRecord[] { record }; }, delegate (DnsResourceRecordType key, IReadOnlyList existingRecords) { foreach (DnsResourceRecord existingRecord in existingRecords) { if (record.RDATA.Equals(existingRecord.RDATA)) return existingRecords; } List updatedRecords = new List(existingRecords.Count + 1); foreach (DnsResourceRecord existingRecord in existingRecords) { if (existingRecord.OriginalTtlValue == record.OriginalTtlValue) { updatedRecords.Add(existingRecord); } else { DnsResourceRecord updatedExistingRecord = new DnsResourceRecord(existingRecord.Name, existingRecord.Type, existingRecord.Class, record.OriginalTtlValue, existingRecord.RDATA); updatedRecords.Add(updatedExistingRecord); added.Add(updatedExistingRecord); deleted.Add(existingRecord); } } updatedRecords.Add(record); added.Add(record); return updatedRecords; }); } #endregion #region catalog zones protected IEnumerable> EnumerateCatalogMemberZones(DnsServer dnsServer) { List subDomains = new List(); dnsServer.AuthZoneManager.ListSubDomains("zones." + _name, subDomains); foreach (string subDomain in subDomains) { IReadOnlyList ptrRecords = dnsServer.AuthZoneManager.GetRecords(_name, subDomain + ".zones." + _name, DnsResourceRecordType.PTR); if (ptrRecords.Count > 0) yield return new KeyValuePair((ptrRecords[0].RDATA as DnsPTRRecordData).Domain, ptrRecords[0].Name); } } #endregion #region DNSSEC internal IReadOnlyList SignAllRRSets() { List rrsigRecords = new List(_entries.Count); foreach (KeyValuePair> entry in _entries) { if (entry.Key == DnsResourceRecordType.RRSIG) continue; rrsigRecords.AddRange(SignRRSet(entry.Value)); } return rrsigRecords; } internal IReadOnlyList RemoveAllDnssecRecords() { List allRemovedRecords = new List(); foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: if (_entries.TryRemove(entry.Key, out IReadOnlyList removedRecords)) allRemovedRecords.AddRange(removedRecords); break; } } return allRemovedRecords; } internal IReadOnlyList RemoveNSecRecordsWithRRSig() { List allRemovedRecords = new List(2); foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.NSEC: if (_entries.TryRemove(entry.Key, out IReadOnlyList removedRecords)) allRemovedRecords.AddRange(removedRecords); break; case DnsResourceRecordType.RRSIG: List recordsToRemove = new List(1); foreach (DnsResourceRecord rrsigRecord in entry.Value) { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; if (rrsig.TypeCovered == DnsResourceRecordType.NSEC) recordsToRemove.Add(rrsigRecord); } if (recordsToRemove.Count > 0) { if (TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToRemove, out IReadOnlyList deletedRecords)) allRemovedRecords.AddRange(deletedRecords); } break; } } return allRemovedRecords; } internal IReadOnlyList RemoveNSec3RecordsWithRRSig() { List allRemovedRecords = new List(2); foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: if (_entries.TryRemove(entry.Key, out IReadOnlyList removedRecords)) allRemovedRecords.AddRange(removedRecords); break; case DnsResourceRecordType.RRSIG: List recordsToRemove = new List(1); foreach (DnsResourceRecord rrsigRecord in entry.Value) { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; switch (rrsig.TypeCovered) { case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: recordsToRemove.Add(rrsigRecord); break; } } if (recordsToRemove.Count > 0) { if (TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToRemove, out IReadOnlyList deletedRecords)) allRemovedRecords.AddRange(deletedRecords); } break; } } return allRemovedRecords; } internal bool HasOnlyNSec3Records() { if (!_entries.ContainsKey(DnsResourceRecordType.NSEC3)) return false; foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.RRSIG: break; default: //found non NSEC3 records return false; } } return true; } internal IReadOnlyList RefreshSignatures() { if (!_entries.TryGetValue(DnsResourceRecordType.RRSIG, out IReadOnlyList rrsigRecords)) { if ((_entries.Count == 1) && _entries.TryGetValue(DnsResourceRecordType.NS, out _)) return Array.Empty(); //delegation NS records are not signed throw new InvalidOperationException(); } List typesToRefresh = new List(); DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord rrsigRecord in rrsigRecords) { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; uint signatureValidityPeriod = rrsig.SignatureExpiration - rrsig.SignatureInception; uint refreshPeriod = signatureValidityPeriod / 3; if (utcNow > DateTime.UnixEpoch.AddSeconds(rrsig.SignatureExpiration - refreshPeriod)) typesToRefresh.Add(rrsig.TypeCovered); } List newRRSigRecords = new List(typesToRefresh.Count); foreach (DnsResourceRecordType type in typesToRefresh) { if (_entries.TryGetValue(type, out IReadOnlyList records)) newRRSigRecords.AddRange(SignRRSet(records)); } return newRRSigRecords; } internal virtual IReadOnlyList SignRRSet(IReadOnlyList records) { throw new NotImplementedException(); } internal IReadOnlyList GetUpdatedNSecRRSet(string nextDomainName, uint ttl) { List types = new List(_entries.Count); foreach (KeyValuePair> entry in _entries) types.Add(entry.Key); if (!types.Contains(DnsResourceRecordType.NSEC)) { types.Add(DnsResourceRecordType.NSEC); if (!types.Contains(DnsResourceRecordType.RRSIG)) types.Add(DnsResourceRecordType.RRSIG); } types.Sort(); DnsNSECRecordData newNSecRecord = new DnsNSECRecordData(nextDomainName, types); if (!_entries.TryGetValue(DnsResourceRecordType.NSEC, out IReadOnlyList existingRecords) || (existingRecords[0].TTL != ttl) || !existingRecords[0].RDATA.Equals(newNSecRecord)) return new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NSEC, DnsClass.IN, ttl, newNSecRecord) }; return Array.Empty(); } internal IReadOnlyList GetUpdatedNSec3RRSet(IReadOnlyList newNSec3Records) { if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3, out IReadOnlyList existingRecords) || (existingRecords[0].TTL != newNSec3Records[0].TTL) || !existingRecords[0].RDATA.Equals(newNSec3Records[0].RDATA)) return newNSec3Records; return Array.Empty(); } internal IReadOnlyList CreateNSec3RRSet(string hashedOwnerName, byte[] nextHashedOwnerName, uint ttl, ushort iterations, byte[] salt) { List types = new List(_entries.Count); foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.NSEC3: //rare case when there is a record created at the same name as that of an existing NSEC3 continue; default: types.Add(entry.Key); break; } } types.Sort(); DnsNSEC3RecordData newNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, nextHashedOwnerName, types); return new DnsResourceRecord[] { new DnsResourceRecord(hashedOwnerName, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newNSec3) }; } internal DnsResourceRecord GetPartialNSec3Record(string zoneName, uint ttl, ushort iterations, byte[] salt) { List types = new List(_entries.Count); foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.NSEC3: //rare case when there is a record created at the same name as that of an existing NSEC3 continue; default: types.Add(entry.Key); break; } } if (_name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) { if (!types.Contains(DnsResourceRecordType.NSEC3PARAM)) types.Add(DnsResourceRecordType.NSEC3PARAM); //add NSEC3PARAM type to NSEC3 for unsigned zone apex } types.Sort(); DnsNSEC3RecordData newNSec3Record = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty(), types); return new DnsResourceRecord(newNSec3Record.ComputeHashedOwnerName(_name) + (zoneName.Length > 0 ? "." + zoneName : ""), DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newNSec3Record); } #endregion #region public public void SyncRecords(Dictionary> newEntries) { //remove entires of type that do not exists in new entries foreach (KeyValuePair> entry in _entries) { if (!newEntries.ContainsKey(entry.Key)) _entries.TryRemove(entry.Key, out _); } //set new entries into zone if (this is ForwarderZone) { //skip NS and SOA records from being added to ForwarderZone foreach (KeyValuePair> newEntry in newEntries) { switch (newEntry.Key) { case DnsResourceRecordType.NS: case DnsResourceRecordType.SOA: break; default: _entries[newEntry.Key] = newEntry.Value; break; } } } else { foreach (KeyValuePair> newEntry in newEntries) { if (newEntry.Key == DnsResourceRecordType.SOA) { if (newEntry.Value.Count != 1) continue; //skip invalid SOA record if (this is SecondaryZone) { //copy existing SOA record's info to new SOA record DnsResourceRecord existingSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsResourceRecord newSoaRecord = newEntry.Value[0]; newSoaRecord.CopyRecordInfoFrom(existingSoaRecord); } } _entries[newEntry.Key] = newEntry.Value; } } } public void SyncRecords(Dictionary> deletedEntries, Dictionary> addedEntries) { if (deletedEntries is not null) { foreach (KeyValuePair> deletedEntry in deletedEntries) { if (_entries.TryGetValue(deletedEntry.Key, out IReadOnlyList existingRecords)) { List updatedRecords = new List(Math.Max(0, existingRecords.Count - deletedEntry.Value.Count)); foreach (DnsResourceRecord existingRecord in existingRecords) { bool deleted = false; foreach (DnsResourceRecord deletedRecord in deletedEntry.Value) { if (existingRecord.RDATA.Equals(deletedRecord.RDATA)) { deleted = true; break; } } if (!deleted) updatedRecords.Add(existingRecord); } if (existingRecords.Count > updatedRecords.Count) { if (updatedRecords.Count > 0) _entries[deletedEntry.Key] = updatedRecords; else _entries.TryRemove(deletedEntry.Key, out _); } } } } if (addedEntries is not null) { foreach (KeyValuePair> addedEntry in addedEntries) { _entries.AddOrUpdate(addedEntry.Key, addedEntry.Value, delegate (DnsResourceRecordType key, IReadOnlyList existingRecords) { List updatedRecords = new List(existingRecords.Count + addedEntry.Value.Count); updatedRecords.AddRange(existingRecords); foreach (DnsResourceRecord addedRecord in addedEntry.Value) { bool exists = false; foreach (DnsResourceRecord existingRecord in existingRecords) { if (addedRecord.RDATA.Equals(existingRecord.RDATA)) { exists = true; break; } } if (!exists) updatedRecords.Add(addedRecord); } if (updatedRecords.Count > existingRecords.Count) return updatedRecords; else return existingRecords; }); } } } public void SyncGlueRecords(IReadOnlyCollection deletedGlueRecords, IReadOnlyCollection addedGlueRecords) { if (_entries.TryGetValue(DnsResourceRecordType.NS, out IReadOnlyList nsRecords)) { foreach (DnsResourceRecord nsRecord in nsRecords) nsRecord.SyncGlueRecords(deletedGlueRecords, addedGlueRecords); } } public void LoadRecords(DnsResourceRecordType type, IReadOnlyList records) { _entries[type] = records; } public virtual void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { switch (type) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DNAME: case DnsResourceRecordType.APP: if ((!_entries.IsEmpty) && !_entries.ContainsKey(type)) throw new InvalidOperationException($"Cannot add record: {type} record already exists for the same name."); break; case DnsResourceRecordType.NSEC: case DnsResourceRecordType.RRSIG: break; //ignore default: if (_entries.ContainsKey(DnsResourceRecordType.CNAME)) throw new InvalidOperationException("Cannot add record: a CNAME record cannot exists with other record types for the same name."); break; } _entries[type] = records; } public virtual bool AddRecord(DnsResourceRecord record) { AddRecord(record, out IReadOnlyList addedRecords, out _); return addedRecords.Count > 0; } public virtual bool DeleteRecords(DnsResourceRecordType type) { return _entries.TryRemove(type, out _); } public virtual bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata) { return TryDeleteRecord(type, rdata, out _); } public virtual void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { if (oldRecord.Type == DnsResourceRecordType.SOA) throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record"); if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); if (!DeleteRecord(oldRecord.Type, oldRecord.RDATA)) throw new DnsWebServiceException("Cannot update record: the old record does not exists."); AddRecord(newRecord); } public virtual IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { switch (type) { case DnsResourceRecordType.APP: case DnsResourceRecordType.FWD: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: { //return only exact type if exists if (_entries.TryGetValue(type, out IReadOnlyList existingRecords)) { IReadOnlyList filteredRecords = FilterDisabledRecords(type, existingRecords); if (filteredRecords.Count > 0) { if (dnssecOk) return AppendRRSigTo(filteredRecords); return filteredRecords; } } } break; case DnsResourceRecordType.ANY: List records = new List(_entries.Count * 2); foreach (KeyValuePair> entry in _entries) { switch (entry.Key) { case DnsResourceRecordType.FWD: case DnsResourceRecordType.APP: //skip records continue; default: records.AddRange(entry.Value); break; } } return FilterDisabledRecords(type, records); default: { //check for CNAME if (_entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList existingCNAMERecords)) { IReadOnlyList filteredRecords = FilterDisabledRecords(type, existingCNAMERecords); if (filteredRecords.Count > 0) { if (dnssecOk) return AppendRRSigTo(filteredRecords); return filteredRecords; } } //check for exact type if (_entries.TryGetValue(type, out IReadOnlyList existingRecords)) { IReadOnlyList filteredRecords = FilterDisabledRecords(type, existingRecords); if (filteredRecords.Count > 0) { if (dnssecOk) return AppendRRSigTo(filteredRecords); return filteredRecords; } } //check special processing switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: //check for ANAME if (_entries.TryGetValue(DnsResourceRecordType.ANAME, out IReadOnlyList anameRecords)) return FilterDisabledRecords(type, anameRecords); //check for ALIAS if (_entries.TryGetValue(DnsResourceRecordType.ALIAS, out IReadOnlyList aliasRecords)) { List newAliasRecords = new List(aliasRecords.Count); foreach (DnsResourceRecord aliasRecord in aliasRecords) { if ((aliasRecord.RDATA is DnsALIASRecordData alias) && (alias.Type == type)) newAliasRecords.Add(aliasRecord); } if (newAliasRecords.Count > 0) return FilterDisabledRecords(type, newAliasRecords); } break; } } break; } return Array.Empty(); } public IReadOnlyList QueryRecordsWildcard(DnsResourceRecordType type, bool dnssecOk, string queryDomain) { IReadOnlyList answers = QueryRecords(type, dnssecOk); if ((answers.Count > 0) && _name.StartsWith('*') && !_name.Equals(queryDomain, StringComparison.OrdinalIgnoreCase)) { //wildcard zone; generate new answer records DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answers.Count]; for (int i = 0; i < answers.Count; i++) wildcardAnswers[i] = new DnsResourceRecord(queryDomain, answers[i].Type, answers[i].Class, answers[i].TTL, answers[i].RDATA) { Tag = answers[i].Tag }; answers = wildcardAnswers; } return answers; } public IReadOnlyList GetRecords(DnsResourceRecordType type) { if (_entries.TryGetValue(type, out IReadOnlyList records)) return records; return Array.Empty(); } public override bool ContainsNameServerRecords() { if (!_entries.TryGetValue(DnsResourceRecordType.NS, out IReadOnlyList records)) return false; foreach (DnsResourceRecord record in records) { if (record.GetAuthGenericRecordInfo().Disabled) continue; return true; } return false; } #endregion #region properties public IReadOnlyDictionary> Entries { get { return _entries; } } public virtual bool Disabled { get { return _disabled; } set { _disabled = value; } } public virtual bool IsActive { get { return !_disabled; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/AuthZoneInfo.cs ================================================ /* 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.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { public enum AuthZoneType : byte { Unknown = 0, Primary = 1, Secondary = 2, Stub = 3, Forwarder = 4, SecondaryForwarder = 5, Catalog = 6, SecondaryCatalog = 7 } public sealed class AuthZoneInfo : IComparable { #region variables readonly ApexZone _apexZone; readonly string _name; readonly AuthZoneType _type; readonly DateTime _lastModified; readonly bool _disabled; readonly string _catalogZoneName; readonly bool _overrideCatalogQueryAccess; readonly bool _overrideCatalogZoneTransfer; readonly bool _overrideCatalogNotify; readonly bool _overrideCatalogPrimaryNameServers; //only for secondary zones readonly AuthZoneQueryAccess _queryAccess; readonly IReadOnlyCollection _queryAccessNetworkACL; readonly AuthZoneTransfer _zoneTransfer; readonly IReadOnlyCollection _zoneTransferNetworkACL; readonly IReadOnlySet _zoneTransferTsigKeyNames; readonly IReadOnlyList _zoneHistory; //for IXFR support readonly AuthZoneNotify _notify; readonly IReadOnlyCollection _notifyNameServers; readonly IReadOnlyCollection _notifySecondaryCatalogNameServers; readonly AuthZoneUpdate _update; readonly IReadOnlyCollection _updateNetworkACL; readonly IReadOnlyDictionary>> _updateSecurityPolicies; readonly IReadOnlyCollection _dnssecPrivateKeys; //only for primary zones readonly IReadOnlyList _primaryNameServerAddresses; //only for secondary and stub zones readonly DnsTransportProtocol _primaryZoneTransferProtocol; //only for secondary zones readonly string _primaryZoneTransferTsigKeyName; //only for secondary zones readonly DateTime _expiry; //only for secondary and stub zones readonly bool _validateZone; //only for secondary zones readonly bool _validationFailed; //only for secondary zones #endregion #region constructor public AuthZoneInfo(string name, AuthZoneType type, bool disabled) { _name = name; _type = type; _lastModified = DateTime.UtcNow; _disabled = disabled; _queryAccess = AuthZoneQueryAccess.Allow; switch (_type) { case AuthZoneType.Primary: _zoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers; _notify = AuthZoneNotify.ZoneNameServers; _update = AuthZoneUpdate.Deny; break; default: _zoneTransfer = AuthZoneTransfer.Deny; _notify = AuthZoneNotify.None; _update = AuthZoneUpdate.Deny; break; } } public AuthZoneInfo(BinaryReader bR, DateTime lastModified) { byte version = bR.ReadByte(); switch (version) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: { _name = bR.ReadShortString(); _type = (AuthZoneType)bR.ReadByte(); _disabled = bR.ReadBoolean(); _queryAccess = AuthZoneQueryAccess.Allow; if (version >= 2) { { _zoneTransfer = (AuthZoneTransfer)bR.ReadByte(); int count = bR.ReadByte(); if (count > 0) { NetworkAddress[] networks = new NetworkAddress[count]; if (version >= 9) { for (int i = 0; i < count; i++) networks[i] = NetworkAddress.ReadFrom(bR); } else { for (int i = 0; i < count; i++) { IPAddress address = IPAddressExtensions.ReadFrom(bR); switch (address.AddressFamily) { case AddressFamily.InterNetwork: networks[i] = new NetworkAddress(address, 32); break; case AddressFamily.InterNetworkV6: networks[i] = new NetworkAddress(address, 128); break; default: throw new InvalidOperationException(); } } } _zoneTransferNetworkACL = ConvertDenyAllowToACL(null, networks); } } { _notify = (AuthZoneNotify)bR.ReadByte(); int count = bR.ReadByte(); if (count > 0) { IPAddress[] nameServers = new IPAddress[count]; for (int i = 0; i < count; i++) nameServers[i] = IPAddressExtensions.ReadFrom(bR); _notifyNameServers = nameServers; } } if (version >= 6) { _update = (AuthZoneUpdate)bR.ReadByte(); int count = bR.ReadByte(); if (count > 0) { NetworkAddress[] networks = new NetworkAddress[count]; if (version >= 9) { for (int i = 0; i < count; i++) networks[i] = NetworkAddress.ReadFrom(bR); } else { for (int i = 0; i < count; i++) { IPAddress address = IPAddressExtensions.ReadFrom(bR); switch (address.AddressFamily) { case AddressFamily.InterNetwork: networks[i] = new NetworkAddress(address, 32); break; case AddressFamily.InterNetworkV6: networks[i] = new NetworkAddress(address, 128); break; default: throw new InvalidOperationException(); } } } _updateNetworkACL = ConvertDenyAllowToACL(null, networks); } } } else { switch (_type) { case AuthZoneType.Primary: _zoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers; _notify = AuthZoneNotify.ZoneNameServers; _update = AuthZoneUpdate.Deny; break; default: _zoneTransfer = AuthZoneTransfer.Deny; _notify = AuthZoneNotify.None; _update = AuthZoneUpdate.Deny; break; } } if (version >= 8) _lastModified = bR.ReadDateTime(); else _lastModified = lastModified; switch (_type) { case AuthZoneType.Primary: { if (version >= 3) { int count = bR.ReadInt32(); DnsResourceRecord[] zoneHistory = new DnsResourceRecord[count]; if (version >= 11) { for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); if (bR.ReadBoolean()) zoneHistory[i].Tag = new HistoryRecordInfo(bR); } } else { for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); zoneHistory[i].Tag = new HistoryRecordInfo(bR); } } _zoneHistory = zoneHistory; } if (version >= 4) { int count = bR.ReadByte(); HashSet tsigKeyNames = new HashSet(count); for (int i = 0; i < count; i++) tsigKeyNames.Add(bR.ReadShortString()); _zoneTransferTsigKeyNames = tsigKeyNames; } if (version >= 7) { int count = bR.ReadByte(); Dictionary>> updateSecurityPolicies = new Dictionary>>(count); for (int i = 0; i < count; i++) { string tsigKeyName = bR.ReadShortString().ToLowerInvariant(); if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary> policyMap)) { policyMap = new Dictionary>(); updateSecurityPolicies.Add(tsigKeyName, policyMap); } int policyCount = bR.ReadByte(); for (int j = 0; j < policyCount; j++) { string domain = bR.ReadShortString().ToLowerInvariant(); if (!policyMap.TryGetValue(domain, out IReadOnlyList types)) { types = new List(); (policyMap as Dictionary>).Add(domain, types); } int typeCount = bR.ReadByte(); for (int k = 0; k < typeCount; k++) (types as List).Add((DnsResourceRecordType)bR.ReadUInt16()); } } _updateSecurityPolicies = updateSecurityPolicies; } else if (version >= 6) { int count = bR.ReadByte(); Dictionary>> updateSecurityPolicies = new Dictionary>>(count); Dictionary> defaultAllowPolicy = new Dictionary>(1); defaultAllowPolicy.Add(_name, new List() { DnsResourceRecordType.ANY }); defaultAllowPolicy.Add("*." + _name, new List() { DnsResourceRecordType.ANY }); for (int i = 0; i < count; i++) updateSecurityPolicies.Add(bR.ReadShortString().ToLowerInvariant(), defaultAllowPolicy); _updateSecurityPolicies = updateSecurityPolicies; } if (version >= 5) { int count = bR.ReadByte(); if (count > 0) { List dnssecPrivateKeys = new List(count); for (int i = 0; i < count; i++) dnssecPrivateKeys.Add(DnssecPrivateKey.ReadFrom(bR)); _dnssecPrivateKeys = dnssecPrivateKeys; } } } break; case AuthZoneType.Secondary: { _expiry = bR.ReadDateTime(); if (version >= 4) { int count = bR.ReadInt32(); DnsResourceRecord[] zoneHistory = new DnsResourceRecord[count]; if (version >= 11) { for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); if (bR.ReadBoolean()) zoneHistory[i].Tag = new HistoryRecordInfo(bR); } } else { for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); zoneHistory[i].Tag = new HistoryRecordInfo(bR); } } _zoneHistory = zoneHistory; } if (version >= 4) { int count = bR.ReadByte(); HashSet tsigKeyNames = new HashSet(count); for (int i = 0; i < count; i++) tsigKeyNames.Add(bR.ReadShortString()); _zoneTransferTsigKeyNames = tsigKeyNames; } if (version == 6) { //MUST skip old version data int count = bR.ReadByte(); Dictionary tsigKeyNames = new Dictionary(count); for (int i = 0; i < count; i++) tsigKeyNames.Add(bR.ReadShortString(), null); } } break; case AuthZoneType.Stub: { _expiry = bR.ReadDateTime(); } break; case AuthZoneType.Forwarder: { if (version >= 10) { int count = bR.ReadByte(); Dictionary>> updateSecurityPolicies = new Dictionary>>(count); for (int i = 0; i < count; i++) { string tsigKeyName = bR.ReadShortString().ToLowerInvariant(); if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary> policyMap)) { policyMap = new Dictionary>(); updateSecurityPolicies.Add(tsigKeyName, policyMap); } int policyCount = bR.ReadByte(); for (int j = 0; j < policyCount; j++) { string domain = bR.ReadShortString().ToLowerInvariant(); if (!policyMap.TryGetValue(domain, out IReadOnlyList types)) { types = new List(); (policyMap as Dictionary>).Add(domain, types); } int typeCount = bR.ReadByte(); for (int k = 0; k < typeCount; k++) (types as List).Add((DnsResourceRecordType)bR.ReadUInt16()); } } _updateSecurityPolicies = updateSecurityPolicies; } } break; } } break; case 12: case 13: case 14: { _name = bR.ReadShortString(); _type = (AuthZoneType)bR.ReadByte(); _lastModified = bR.ReadDateTime(); _disabled = bR.ReadBoolean(); switch (_type) { case AuthZoneType.Primary: _catalogZoneName = bR.ReadShortString(); if (_catalogZoneName.Length == 0) _catalogZoneName = null; _overrideCatalogQueryAccess = bR.ReadBoolean(); _overrideCatalogZoneTransfer = bR.ReadBoolean(); _overrideCatalogNotify = bR.ReadBoolean(); _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _zoneTransfer = (AuthZoneTransfer)bR.ReadByte(); _zoneTransferNetworkACL = ReadNetworkACLFrom(bR); _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR); _zoneHistory = ReadZoneHistoryFrom(bR); _notify = (AuthZoneNotify)bR.ReadByte(); _notifyNameServers = ReadIPAddressesFrom(bR); _update = (AuthZoneUpdate)bR.ReadByte(); _updateNetworkACL = ReadNetworkACLFrom(bR); _updateSecurityPolicies = ReadUpdateSecurityPoliciesFrom(bR); _dnssecPrivateKeys = ReadDnssecPrivateKeysFrom(bR); break; case AuthZoneType.Secondary: _catalogZoneName = bR.ReadShortString(); if (_catalogZoneName.Length == 0) _catalogZoneName = null; _overrideCatalogQueryAccess = bR.ReadBoolean(); _overrideCatalogZoneTransfer = bR.ReadBoolean(); _overrideCatalogPrimaryNameServers = bR.ReadBoolean(); _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _zoneTransfer = (AuthZoneTransfer)bR.ReadByte(); _zoneTransferNetworkACL = ReadNetworkACLFrom(bR); _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR); _zoneHistory = ReadZoneHistoryFrom(bR); _notify = (AuthZoneNotify)bR.ReadByte(); _notifyNameServers = ReadIPAddressesFrom(bR); _update = (AuthZoneUpdate)bR.ReadByte(); _updateNetworkACL = ReadNetworkACLFrom(bR); if (version >= 14) _dnssecPrivateKeys = ReadDnssecPrivateKeysFrom(bR); _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR); _primaryZoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte(); _primaryZoneTransferTsigKeyName = bR.ReadShortString(); if (_primaryZoneTransferTsigKeyName.Length == 0) _primaryZoneTransferTsigKeyName = null; _expiry = bR.ReadDateTime(); _validateZone = bR.ReadBoolean(); _validationFailed = bR.ReadBoolean(); break; case AuthZoneType.Stub: _catalogZoneName = bR.ReadShortString(); if (_catalogZoneName.Length == 0) _catalogZoneName = null; _overrideCatalogQueryAccess = bR.ReadBoolean(); _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR); _expiry = bR.ReadDateTime(); break; case AuthZoneType.Forwarder: _catalogZoneName = bR.ReadShortString(); if (_catalogZoneName.Length == 0) _catalogZoneName = null; _overrideCatalogQueryAccess = bR.ReadBoolean(); _overrideCatalogZoneTransfer = bR.ReadBoolean(); _overrideCatalogNotify = bR.ReadBoolean(); _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _zoneTransfer = (AuthZoneTransfer)bR.ReadByte(); _zoneTransferNetworkACL = ReadNetworkACLFrom(bR); _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR); _zoneHistory = ReadZoneHistoryFrom(bR); _notify = (AuthZoneNotify)bR.ReadByte(); _notifyNameServers = ReadIPAddressesFrom(bR); _update = (AuthZoneUpdate)bR.ReadByte(); _updateNetworkACL = ReadNetworkACLFrom(bR); _updateSecurityPolicies = ReadUpdateSecurityPoliciesFrom(bR); break; case AuthZoneType.SecondaryForwarder: _catalogZoneName = bR.ReadShortString(); if (_catalogZoneName.Length == 0) _catalogZoneName = null; _overrideCatalogQueryAccess = bR.ReadBoolean(); _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _update = (AuthZoneUpdate)bR.ReadByte(); _updateNetworkACL = ReadNetworkACLFrom(bR); _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR); _primaryZoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte(); _primaryZoneTransferTsigKeyName = bR.ReadShortString(); if (_primaryZoneTransferTsigKeyName.Length == 0) _primaryZoneTransferTsigKeyName = null; _expiry = bR.ReadDateTime(); break; case AuthZoneType.Catalog: _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _zoneTransfer = (AuthZoneTransfer)bR.ReadByte(); _zoneTransferNetworkACL = ReadNetworkACLFrom(bR); _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR); _zoneHistory = ReadZoneHistoryFrom(bR); _notify = (AuthZoneNotify)bR.ReadByte(); _notifyNameServers = ReadIPAddressesFrom(bR); if (version >= 13) _notifySecondaryCatalogNameServers = ReadIPAddressesFrom(bR); break; case AuthZoneType.SecondaryCatalog: _queryAccess = (AuthZoneQueryAccess)bR.ReadByte(); _queryAccessNetworkACL = ReadNetworkACLFrom(bR); _zoneTransfer = (AuthZoneTransfer)bR.ReadByte(); _zoneTransferNetworkACL = ReadNetworkACLFrom(bR); _zoneTransferTsigKeyNames = ReadZoneTransferTsigKeyNamesFrom(bR); _primaryNameServerAddresses = ReadNameServerAddressesFrom(bR); _primaryZoneTransferProtocol = (DnsTransportProtocol)bR.ReadByte(); _primaryZoneTransferTsigKeyName = bR.ReadShortString(); if (_primaryZoneTransferTsigKeyName.Length == 0) _primaryZoneTransferTsigKeyName = null; _expiry = bR.ReadDateTime(); break; } } break; default: throw new InvalidDataException("AuthZoneInfo format version not supported."); } } internal AuthZoneInfo(ApexZone apexZone, bool loadHistory = false) { _apexZone = apexZone; _name = _apexZone.Name; _lastModified = _apexZone.LastModified; _disabled = _apexZone.Disabled; if (_apexZone is PrimaryZone primaryZone) { _type = AuthZoneType.Primary; _catalogZoneName = _apexZone.CatalogZoneName; _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess; _overrideCatalogZoneTransfer = _apexZone.OverrideCatalogZoneTransfer; _overrideCatalogNotify = _apexZone.OverrideCatalogNotify; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _zoneTransfer = _apexZone.ZoneTransfer; _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames; if (loadHistory) _zoneHistory = _apexZone.GetZoneHistory(); _notify = _apexZone.Notify; _notifyNameServers = _apexZone.NotifyNameServers; _update = _apexZone.Update; _updateNetworkACL = _apexZone.UpdateNetworkACL; _updateSecurityPolicies = _apexZone.UpdateSecurityPolicies; _dnssecPrivateKeys = primaryZone.DnssecPrivateKeys; } else if (_apexZone is SecondaryCatalogZone secondaryCatalogZone) { _type = AuthZoneType.SecondaryCatalog; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _zoneTransfer = _apexZone.ZoneTransfer; _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames; _primaryNameServerAddresses = secondaryCatalogZone.PrimaryNameServerAddresses; _primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol; _primaryZoneTransferTsigKeyName = secondaryCatalogZone.PrimaryZoneTransferTsigKeyName; _expiry = secondaryCatalogZone.Expiry; } else if (_apexZone is SecondaryForwarderZone secondaryForwarderZone) { _type = AuthZoneType.SecondaryForwarder; _catalogZoneName = _apexZone.CatalogZoneName; _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _update = _apexZone.Update; _updateNetworkACL = _apexZone.UpdateNetworkACL; _primaryNameServerAddresses = secondaryForwarderZone.PrimaryNameServerAddresses; _primaryZoneTransferProtocol = secondaryForwarderZone.PrimaryZoneTransferProtocol; _primaryZoneTransferTsigKeyName = secondaryForwarderZone.PrimaryZoneTransferTsigKeyName; _expiry = secondaryForwarderZone.Expiry; } else if (_apexZone is SecondaryZone secondaryZone) { _type = AuthZoneType.Secondary; _catalogZoneName = _apexZone.CatalogZoneName; _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess; _overrideCatalogZoneTransfer = _apexZone.OverrideCatalogZoneTransfer; _overrideCatalogPrimaryNameServers = secondaryZone.OverrideCatalogPrimaryNameServers; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _zoneTransfer = _apexZone.ZoneTransfer; _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames; if (loadHistory) _zoneHistory = _apexZone.GetZoneHistory(); _notify = _apexZone.Notify; _notifyNameServers = _apexZone.NotifyNameServers; _update = _apexZone.Update; _updateNetworkACL = _apexZone.UpdateNetworkACL; _dnssecPrivateKeys = secondaryZone.DnssecPrivateKeys; _primaryNameServerAddresses = secondaryZone.PrimaryNameServerAddresses; _primaryZoneTransferProtocol = secondaryZone.PrimaryZoneTransferProtocol; _primaryZoneTransferTsigKeyName = secondaryZone.PrimaryZoneTransferTsigKeyName; _expiry = secondaryZone.Expiry; _validateZone = secondaryZone.ValidateZone; _validationFailed = secondaryZone.ValidationFailed; } else if (_apexZone is StubZone stubZone) { _type = AuthZoneType.Stub; _catalogZoneName = _apexZone.CatalogZoneName; _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _primaryNameServerAddresses = stubZone.PrimaryNameServerAddresses; _expiry = stubZone.Expiry; } else if (_apexZone is CatalogZone) { _type = AuthZoneType.Catalog; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _zoneTransfer = _apexZone.ZoneTransfer; _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames; if (loadHistory) _zoneHistory = _apexZone.GetZoneHistory(); _notify = _apexZone.Notify; _notifyNameServers = _apexZone.NotifyNameServers; _notifySecondaryCatalogNameServers = _apexZone.NotifySecondaryCatalogNameServers; } else if (_apexZone is ForwarderZone) { _type = AuthZoneType.Forwarder; _catalogZoneName = _apexZone.CatalogZoneName; _overrideCatalogQueryAccess = _apexZone.OverrideCatalogQueryAccess; _overrideCatalogZoneTransfer = _apexZone.OverrideCatalogZoneTransfer; _overrideCatalogNotify = _apexZone.OverrideCatalogNotify; _queryAccess = _apexZone.QueryAccess; _queryAccessNetworkACL = _apexZone.QueryAccessNetworkACL; _zoneTransfer = _apexZone.ZoneTransfer; _zoneTransferNetworkACL = _apexZone.ZoneTransferNetworkACL; _zoneTransferTsigKeyNames = _apexZone.ZoneTransferTsigKeyNames; if (loadHistory) _zoneHistory = _apexZone.GetZoneHistory(); _notify = _apexZone.Notify; _notifyNameServers = _apexZone.NotifyNameServers; _update = _apexZone.Update; _updateNetworkACL = _apexZone.UpdateNetworkACL; _updateSecurityPolicies = _apexZone.UpdateSecurityPolicies; } else { _type = AuthZoneType.Unknown; } } #endregion #region static public static string GetZoneTypeName(AuthZoneType type) { switch (type) { case AuthZoneType.SecondaryForwarder: return "Secondary Forwarder"; case AuthZoneType.SecondaryCatalog: return "Secondary Catalog"; default: return type.ToString(); } } internal static NameServerAddress[] ReadNameServerAddressesFrom(BinaryReader bR) { int count = bR.ReadByte(); if (count < 1) return null; NameServerAddress[] nameServerAddresses = new NameServerAddress[count]; for (int i = 0; i < count; i++) nameServerAddresses[i] = new NameServerAddress(bR); return nameServerAddresses; } internal static void WriteNameServerAddressesTo(IReadOnlyCollection nameServerAddresses, BinaryWriter bW) { if (nameServerAddresses is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(nameServerAddresses.Count)); foreach (NameServerAddress network in nameServerAddresses) network.WriteTo(bW); } } internal static NetworkAccessControl[] ReadNetworkACLFrom(BinaryReader bR) { int count = bR.ReadByte(); if (count < 1) return null; NetworkAccessControl[] acl = new NetworkAccessControl[count]; for (int i = 0; i < count; i++) acl[i] = NetworkAccessControl.ReadFrom(bR); return acl; } internal static void WriteNetworkACLTo(IReadOnlyCollection acl, BinaryWriter bW) { if (acl is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(acl.Count)); foreach (NetworkAccessControl nac in acl) nac.WriteTo(bW); } } internal static NetworkAddress[] ReadNetworkAddressesFrom(BinaryReader bR) { int count = bR.ReadByte(); if (count < 1) return null; NetworkAddress[] networks = new NetworkAddress[count]; for (int i = 0; i < count; i++) networks[i] = NetworkAddress.ReadFrom(bR); return networks; } internal static void WriteNetworkAddressesTo(IReadOnlyCollection networkAddresses, BinaryWriter bW) { if (networkAddresses is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(networkAddresses.Count)); foreach (NetworkAddress network in networkAddresses) network.WriteTo(bW); } } internal static IPAddress[] ReadIPAddressesFrom(BinaryReader bR) { int count = bR.ReadByte(); if (count < 1) return null; IPAddress[] ipAddresses = new IPAddress[count]; for (int i = 0; i < count; i++) ipAddresses[i] = IPAddressExtensions.ReadFrom(bR); return ipAddresses; } internal static void WriteIPAddressesTo(IReadOnlyCollection ipAddresses, BinaryWriter bW) { if (ipAddresses is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(ipAddresses.Count)); foreach (IPAddress ipAddress in ipAddresses) ipAddress.WriteTo(bW); } } internal static List ConvertDenyAllowToACL(NetworkAddress[] deniedNetworks, NetworkAddress[] allowedNetworks) { List acl = new List(); if (deniedNetworks is not null) { foreach (NetworkAddress network in deniedNetworks) acl.Add(new NetworkAccessControl(network, true)); } if (allowedNetworks is not null) { foreach (NetworkAddress network in allowedNetworks) acl.Add(new NetworkAccessControl(network)); } if (acl.Count > 0) return acl; return null; } private static HashSet ReadZoneTransferTsigKeyNamesFrom(BinaryReader bR) { int count = bR.ReadByte(); HashSet zoneTransferTsigKeyNames = new HashSet(count); for (int i = 0; i < count; i++) zoneTransferTsigKeyNames.Add(bR.ReadShortString()); return zoneTransferTsigKeyNames; } private static void WriteZoneTransferTsigKeyNamesTo(IReadOnlySet zoneTransferTsigKeyNames, BinaryWriter bW) { if (zoneTransferTsigKeyNames is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(zoneTransferTsigKeyNames.Count)); foreach (string tsigKeyName in zoneTransferTsigKeyNames) bW.WriteShortString(tsigKeyName); } } private static DnsResourceRecord[] ReadZoneHistoryFrom(BinaryReader bR) { int count = bR.ReadInt32(); DnsResourceRecord[] zoneHistory = new DnsResourceRecord[count]; for (int i = 0; i < count; i++) { zoneHistory[i] = new DnsResourceRecord(bR.BaseStream); if (bR.ReadBoolean()) zoneHistory[i].Tag = new HistoryRecordInfo(bR); } return zoneHistory; } private static void WriteZoneHistoryTo(IReadOnlyList zoneHistory, BinaryWriter bW) { if (zoneHistory is null) { bW.Write(0); } else { bW.Write(zoneHistory.Count); foreach (DnsResourceRecord record in zoneHistory) { record.WriteTo(bW.BaseStream); if (record.Tag is HistoryRecordInfo rrInfo) { bW.Write(true); rrInfo.WriteTo(bW); } else { bW.Write(false); } } } } private static Dictionary>> ReadUpdateSecurityPoliciesFrom(BinaryReader bR) { int count = bR.ReadInt32(); Dictionary>> updateSecurityPolicies = new Dictionary>>(count); for (int i = 0; i < count; i++) { string tsigKeyName = bR.ReadShortString().ToLowerInvariant(); if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary> policyMap)) { policyMap = new Dictionary>(); updateSecurityPolicies.Add(tsigKeyName, policyMap); } int policyCount = bR.ReadByte(); for (int j = 0; j < policyCount; j++) { string domain = bR.ReadShortString().ToLowerInvariant(); if (!policyMap.TryGetValue(domain, out IReadOnlyList types)) { types = new List(); (policyMap as Dictionary>).Add(domain, types); } int typeCount = bR.ReadByte(); for (int k = 0; k < typeCount; k++) (types as List).Add((DnsResourceRecordType)bR.ReadUInt16()); } } return updateSecurityPolicies; } private static void WriteUpdateSecurityPoliciesTo(IReadOnlyDictionary>> updateSecurityPolicies, BinaryWriter bW) { if (updateSecurityPolicies is null) { bW.Write(0); } else { bW.Write(updateSecurityPolicies.Count); foreach (KeyValuePair>> updateSecurityPolicy in updateSecurityPolicies) { bW.WriteShortString(updateSecurityPolicy.Key); bW.Write(Convert.ToByte(updateSecurityPolicy.Value.Count)); foreach (KeyValuePair> policyMap in updateSecurityPolicy.Value) { bW.WriteShortString(policyMap.Key); bW.Write(Convert.ToByte(policyMap.Value.Count)); foreach (DnsResourceRecordType type in policyMap.Value) bW.Write((ushort)type); } } } } internal static DnssecPrivateKey[] ReadDnssecPrivateKeysFrom(BinaryReader bR) { int count = bR.ReadByte(); if (count < 1) return null; DnssecPrivateKey[] dnssecPrivateKeys = new DnssecPrivateKey[count]; for (int i = 0; i < count; i++) dnssecPrivateKeys[i] = DnssecPrivateKey.ReadFrom(bR); return dnssecPrivateKeys; } internal static void WriteDnssecPrivateKeysTo(IReadOnlyCollection dnssecPrivateKeys, BinaryWriter bW) { if (dnssecPrivateKeys is null) { bW.Write((byte)0); } else { bW.Write(Convert.ToByte(dnssecPrivateKeys.Count)); foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) dnssecPrivateKey.WriteTo(bW); } } #endregion #region public public void TriggerRefresh() { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: (_apexZone as SecondaryZone).TriggerRefresh(); break; case AuthZoneType.Stub: (_apexZone as StubZone).TriggerRefresh(); break; default: throw new InvalidOperationException(); } } public void TriggerResync() { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: (_apexZone as SecondaryZone).TriggerResync(); break; case AuthZoneType.Stub: (_apexZone as StubZone).TriggerResync(); break; default: throw new InvalidOperationException(); } } public void WriteTo(BinaryWriter bW) { if (_apexZone is null) throw new InvalidOperationException(); bW.Write((byte)14); //version bW.WriteShortString(_name); bW.Write((byte)_type); bW.Write(_lastModified); bW.Write(_disabled); switch (_type) { case AuthZoneType.Primary: bW.Write(_catalogZoneName ?? ""); bW.Write(_overrideCatalogQueryAccess); bW.Write(_overrideCatalogZoneTransfer); bW.Write(_overrideCatalogNotify); bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); bW.Write((byte)_zoneTransfer); WriteNetworkACLTo(_zoneTransferNetworkACL, bW); WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW); WriteZoneHistoryTo(_zoneHistory, bW); bW.Write((byte)_notify); WriteIPAddressesTo(_notifyNameServers, bW); bW.Write((byte)_update); WriteNetworkACLTo(_updateNetworkACL, bW); WriteUpdateSecurityPoliciesTo(_updateSecurityPolicies, bW); WriteDnssecPrivateKeysTo(_dnssecPrivateKeys, bW); break; case AuthZoneType.Secondary: bW.Write(_catalogZoneName ?? ""); bW.Write(_overrideCatalogQueryAccess); bW.Write(_overrideCatalogZoneTransfer); bW.Write(_overrideCatalogPrimaryNameServers); bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); bW.Write((byte)_zoneTransfer); WriteNetworkACLTo(_zoneTransferNetworkACL, bW); WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW); WriteZoneHistoryTo(_zoneHistory, bW); bW.Write((byte)_notify); WriteIPAddressesTo(_notifyNameServers, bW); bW.Write((byte)_update); WriteNetworkACLTo(_updateNetworkACL, bW); WriteDnssecPrivateKeysTo(_dnssecPrivateKeys, bW); WriteNameServerAddressesTo(_primaryNameServerAddresses, bW); bW.Write((byte)_primaryZoneTransferProtocol); bW.Write(_primaryZoneTransferTsigKeyName ?? ""); bW.Write(_expiry); bW.Write(_validateZone); bW.Write(_validationFailed); break; case AuthZoneType.Stub: bW.Write(_catalogZoneName ?? ""); bW.Write(_overrideCatalogQueryAccess); bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); WriteNameServerAddressesTo(_primaryNameServerAddresses, bW); bW.Write(_expiry); break; case AuthZoneType.Forwarder: bW.Write(_catalogZoneName ?? ""); bW.Write(_overrideCatalogQueryAccess); bW.Write(_overrideCatalogZoneTransfer); bW.Write(_overrideCatalogNotify); bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); bW.Write((byte)_zoneTransfer); WriteNetworkACLTo(_zoneTransferNetworkACL, bW); WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW); WriteZoneHistoryTo(_zoneHistory, bW); bW.Write((byte)_notify); WriteIPAddressesTo(_notifyNameServers, bW); bW.Write((byte)_update); WriteNetworkACLTo(_updateNetworkACL, bW); WriteUpdateSecurityPoliciesTo(_updateSecurityPolicies, bW); break; case AuthZoneType.SecondaryForwarder: bW.Write(_catalogZoneName ?? ""); bW.Write(_overrideCatalogQueryAccess); bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); bW.Write((byte)_update); WriteNetworkACLTo(_updateNetworkACL, bW); WriteNameServerAddressesTo(_primaryNameServerAddresses, bW); bW.Write((byte)_primaryZoneTransferProtocol); bW.Write(_primaryZoneTransferTsigKeyName ?? ""); bW.Write(_expiry); break; case AuthZoneType.Catalog: bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); bW.Write((byte)_zoneTransfer); WriteNetworkACLTo(_zoneTransferNetworkACL, bW); WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW); WriteZoneHistoryTo(_zoneHistory, bW); bW.Write((byte)_notify); WriteIPAddressesTo(_notifyNameServers, bW); WriteIPAddressesTo(_notifySecondaryCatalogNameServers, bW); break; case AuthZoneType.SecondaryCatalog: bW.Write((byte)_queryAccess); WriteNetworkACLTo(_queryAccessNetworkACL, bW); bW.Write((byte)_zoneTransfer); WriteNetworkACLTo(_zoneTransferNetworkACL, bW); WriteZoneTransferTsigKeyNamesTo(_zoneTransferTsigKeyNames, bW); WriteNameServerAddressesTo(_primaryNameServerAddresses, bW); bW.Write((byte)_primaryZoneTransferProtocol); bW.Write(_primaryZoneTransferTsigKeyName ?? ""); bW.Write(_expiry); break; } } public int CompareTo(AuthZoneInfo other) { return _name.CompareTo(other._name); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (obj is not AuthZoneInfo other) return false; return _name.Equals(other._name, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() { return HashCode.Combine(_name); } public override string ToString() { return _name.Length == 0 ? "" : _name; ; } #endregion #region properties internal ApexZone ApexZone { get { return _apexZone; } } public string Name { get { return _name; } } public string DisplayName { get { return _name.Length == 0 ? "" : _name; } } public AuthZoneType Type { get { return _type; } } public string TypeName { get { return GetZoneTypeName(_type); } } public DateTime LastModified { get { if (_apexZone is null) return _lastModified; return _apexZone.LastModified; } } public bool Disabled { get { if (_apexZone is null) return _disabled; return _apexZone.Disabled; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.Disabled = value; } } public string CatalogZoneName { get { if (_apexZone is null) return _catalogZoneName; return _apexZone.CatalogZoneName; } } public bool OverrideCatalogQueryAccess { get { if (_apexZone is null) return _overrideCatalogQueryAccess; return _apexZone.OverrideCatalogQueryAccess; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.OverrideCatalogQueryAccess = value; } } public bool OverrideCatalogZoneTransfer { get { if (_apexZone is null) return _overrideCatalogZoneTransfer; return _apexZone.OverrideCatalogZoneTransfer; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.OverrideCatalogZoneTransfer = value; } } public bool OverrideCatalogNotify { get { if (_apexZone is null) return _overrideCatalogNotify; return _apexZone.OverrideCatalogNotify; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.OverrideCatalogNotify = value; } } public bool OverrideCatalogPrimaryNameServers { get { if (_apexZone is null) return _overrideCatalogPrimaryNameServers; switch (_type) { case AuthZoneType.Secondary: return (_apexZone as SecondaryZone).OverrideCatalogPrimaryNameServers; case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: return false; default: throw new InvalidOperationException(); } } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: (_apexZone as SecondaryZone).OverrideCatalogPrimaryNameServers = value; break; default: throw new InvalidOperationException(); } } } public AuthZoneQueryAccess QueryAccess { get { if (_apexZone is null) return _queryAccess; return _apexZone.QueryAccess; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.QueryAccess = value; } } public IReadOnlyCollection QueryAccessNetworkACL { get { if (_apexZone is null) return _queryAccessNetworkACL; return _apexZone.QueryAccessNetworkACL; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.QueryAccessNetworkACL = value; } } public AuthZoneTransfer ZoneTransfer { get { if (_apexZone is null) return _zoneTransfer; return _apexZone.ZoneTransfer; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.ZoneTransfer = value; } } public IReadOnlyCollection ZoneTransferNetworkACL { get { if (_apexZone is null) return _zoneTransferNetworkACL; return _apexZone.ZoneTransferNetworkACL; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.ZoneTransferNetworkACL = value; } } public IReadOnlySet ZoneTransferTsigKeyNames { get { if (_apexZone is null) return _zoneTransferTsigKeyNames; return _apexZone.ZoneTransferTsigKeyNames; } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: _apexZone.ZoneTransferTsigKeyNames = value; break; default: throw new InvalidOperationException(); } } } public IReadOnlyList ZoneHistory { get { if (_apexZone is null) return _zoneHistory; return _apexZone.GetZoneHistory(); } } public AuthZoneNotify Notify { get { if (_apexZone is null) return _notify; return _apexZone.Notify; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.Notify = value; } } public IReadOnlyCollection NotifyNameServers { get { if (_apexZone is null) return _notifyNameServers; return _apexZone.NotifyNameServers; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.NotifyNameServers = value; } } public IReadOnlyCollection NotifySecondaryCatalogNameServers { get { if (_apexZone is null) return _notifySecondaryCatalogNameServers; return _apexZone.NotifySecondaryCatalogNameServers; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.NotifySecondaryCatalogNameServers = value; } } public AuthZoneUpdate Update { get { if (_apexZone is null) return _update; return _apexZone.Update; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.Update = value; } } public IReadOnlyCollection UpdateNetworkACL { get { if (_apexZone is null) return _updateNetworkACL; return _apexZone.UpdateNetworkACL; } set { if (_apexZone is null) throw new InvalidOperationException(); _apexZone.UpdateNetworkACL = value; } } public IReadOnlyDictionary>> UpdateSecurityPolicies { get { if (_apexZone is null) return _updateSecurityPolicies; return _apexZone.UpdateSecurityPolicies; } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: _apexZone.UpdateSecurityPolicies = value; break; default: throw new InvalidOperationException(); } } } public IReadOnlyCollection DnssecPrivateKeys { get { if (_apexZone is null) return _dnssecPrivateKeys; switch (_type) { case AuthZoneType.Primary: return (_apexZone as PrimaryZone).DnssecPrivateKeys; case AuthZoneType.Secondary: return (_apexZone as SecondaryZone).DnssecPrivateKeys; default: throw new InvalidOperationException(); } } } public IReadOnlyList PrimaryNameServerAddresses { get { if (_apexZone is null) return _primaryNameServerAddresses; switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: return (_apexZone as SecondaryZone).PrimaryNameServerAddresses; case AuthZoneType.Stub: return (_apexZone as StubZone).PrimaryNameServerAddresses; default: throw new InvalidOperationException(); } } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: (_apexZone as SecondaryZone).PrimaryNameServerAddresses = value; break; case AuthZoneType.Stub: (_apexZone as StubZone).PrimaryNameServerAddresses = value; break; default: throw new InvalidOperationException(); } } } public DnsTransportProtocol PrimaryZoneTransferProtocol { get { if (_apexZone is null) return _primaryZoneTransferProtocol; switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: return (_apexZone as SecondaryZone).PrimaryZoneTransferProtocol; default: throw new InvalidOperationException(); } } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: (_apexZone as SecondaryZone).PrimaryZoneTransferProtocol = value; break; default: throw new InvalidOperationException(); } } } public string PrimaryZoneTransferTsigKeyName { get { if (_apexZone is null) return _primaryZoneTransferTsigKeyName; switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: return (_apexZone as SecondaryZone).PrimaryZoneTransferTsigKeyName; default: throw new InvalidOperationException(); } } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: (_apexZone as SecondaryZone).PrimaryZoneTransferTsigKeyName = value; break; default: throw new InvalidOperationException(); } } } public DateTime Expiry { get { if (_apexZone is null) return _expiry; switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: return (_apexZone as SecondaryZone).Expiry; case AuthZoneType.Stub: return (_apexZone as StubZone).Expiry; default: throw new InvalidOperationException(); } } } public bool ValidateZone { get { if (_apexZone is null) return _validateZone; switch (_type) { case AuthZoneType.Secondary: return (_apexZone as SecondaryZone).ValidateZone; default: throw new InvalidOperationException(); } } set { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: (_apexZone as SecondaryZone).ValidateZone = value; break; default: throw new InvalidOperationException(); } } } public bool ValidationFailed { get { if (_apexZone is null) return _validationFailed; switch (_type) { case AuthZoneType.Secondary: return (_apexZone as SecondaryZone).ValidationFailed; default: throw new InvalidOperationException(); } } } public uint DnsKeyTtl { get { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Primary: return (_apexZone as PrimaryZone).GetDnsKeyTtl(); default: throw new InvalidOperationException(); } } } public bool Internal { get { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Primary: return (_apexZone as PrimaryZone).Internal; default: return false; } } } public bool IsExpired { get { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: return (_apexZone as SecondaryZone).IsExpired; case AuthZoneType.Stub: return (_apexZone as StubZone).IsExpired; default: return false; } } } public string[] NotifyFailed { get { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: return _apexZone.NotifyFailed; default: throw new InvalidOperationException(); } } } public bool SyncFailed { get { if (_apexZone is null) throw new InvalidOperationException(); switch (_type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: case AuthZoneType.Stub: return _apexZone.SyncFailed; default: throw new InvalidOperationException(); } } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/CacheZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class CacheZone : Zone { #region variables ConcurrentDictionary>> _ecsEntries; #endregion #region constructor public CacheZone(string name, int capacity) : base(name, capacity) { } private CacheZone(string name, ConcurrentDictionary> entries) : base(name, entries) { } #endregion #region static public static CacheZone ReadFrom(BinaryReader bR, bool serveStale) { byte version = bR.ReadByte(); switch (version) { case 1: string name = bR.ReadString(); ConcurrentDictionary> entries = ReadEntriesFrom(bR, serveStale); CacheZone cacheZone = new CacheZone(name, entries); //write all ECS cache records { int ecsCount = bR.ReadInt32(); if (ecsCount > 0) { ConcurrentDictionary>> ecsEntries = new ConcurrentDictionary>>(1, ecsCount); for (int i = 0; i < ecsCount; i++) { NetworkAddress key = NetworkAddress.ReadFrom(bR); ConcurrentDictionary> ecsEntry = ReadEntriesFrom(bR, serveStale); if (!ecsEntry.IsEmpty) ecsEntries.TryAdd(key, ecsEntry); } if (!ecsEntries.IsEmpty) cacheZone._ecsEntries = ecsEntries; } } return cacheZone; default: throw new InvalidDataException("CacheZone format version not supported."); } } #endregion #region private private static IReadOnlyList ValidateRRSet(DnsResourceRecordType type, IReadOnlyList records, bool serveStale, bool skipSpecialCacheRecord) { foreach (DnsResourceRecord record in records) { if (record.IsExpired(serveStale)) return Array.Empty(); //RR Set is expired if (skipSpecialCacheRecord && (record.RDATA is DnsCache.DnsSpecialCacheRecordData)) return Array.Empty(); //RR Set is special cache record } if (records.Count > 1) { switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: List newRecords = new List(records); newRecords.Shuffle(); //shuffle records to allow load balancing return newRecords; } } //update last used on DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in records) record.GetCacheRecordInfo().LastUsedOn = utcNow; return records; } private static ConcurrentDictionary> ReadEntriesFrom(BinaryReader bR, bool serveStale) { int count = bR.ReadInt32(); ConcurrentDictionary> entries = new ConcurrentDictionary>(1, count); for (int i = 0; i < count; i++) { DnsResourceRecordType key = (DnsResourceRecordType)bR.ReadUInt16(); int rrCount = bR.ReadInt32(); DnsResourceRecord[] records = new DnsResourceRecord[rrCount]; for (int j = 0; j < rrCount; j++) { records[j] = DnsResourceRecord.ReadCacheRecordFrom(bR, delegate (DnsResourceRecord record) { record.Tag = new CacheRecordInfo(bR); }); } if (!DnsResourceRecord.IsRRSetExpired(records, serveStale)) entries.TryAdd(key, records); } return entries; } private static void WriteEntriesTo(ConcurrentDictionary> entries, BinaryWriter bW) { bW.Write(entries.Count); foreach (KeyValuePair> entry in entries) { bW.Write((ushort)entry.Key); bW.Write(entry.Value.Count); foreach (DnsResourceRecord record in entry.Value) { record.WriteCacheRecordTo(bW, delegate () { if (record.Tag is not CacheRecordInfo rrInfo) rrInfo = CacheRecordInfo.Default; //default info rrInfo.WriteTo(bW); }); } } } #endregion #region public public bool SetRecords(DnsResourceRecordType type, IReadOnlyList records, bool serveStale) { if (records.Count == 0) return false; ConcurrentDictionary> entries; CacheRecordInfo cacheRecordInfo = records[0].GetCacheRecordInfo(); NetworkAddress eDnsClientSubnet = cacheRecordInfo.EDnsClientSubnet; if (eDnsClientSubnet is null) { entries = _entries; } else { if (_ecsEntries is null) { _ecsEntries = new ConcurrentDictionary>>(1, 5); entries = new ConcurrentDictionary>(1, 1); if (!_ecsEntries.TryAdd(eDnsClientSubnet, entries)) return false; } else if (!_ecsEntries.TryGetValue(eDnsClientSubnet, out entries)) { entries = new ConcurrentDictionary>(1, 1); if (!_ecsEntries.TryAdd(eDnsClientSubnet, entries)) return false; } } bool isFailureRecord = false; if (records[0].RDATA is DnsCache.DnsSpecialCacheRecordData splRecord) { if (splRecord.IsFailureOrBadCache) { //call trying to cache failure record isFailureRecord = true; if (entries.TryGetValue(type, out IReadOnlyList existingRecords) && (existingRecords.Count > 0) && !DnsResourceRecord.IsRRSetExpired(existingRecords, serveStale)) { if ((existingRecords[0].RDATA is not DnsCache.DnsSpecialCacheRecordData existingSplRecord) || !existingSplRecord.IsFailureOrBadCache) return false; //skip to avoid overwriting a useful record with a failure record //copy extended errors from existing spl record splRecord.CopyExtendedDnsErrorsFrom(existingSplRecord); } } } else if (records[0].Type == DnsResourceRecordType.CHILD_NS) { //convert back RRSet to correct type DnsResourceRecord[] newRecords = new DnsResourceRecord[records.Count]; for (int i = 0; i < records.Count; i++) { DnsResourceRecord record = records[i]; if (record.Type == DnsResourceRecordType.CHILD_NS) record = record.CloneAs(DnsResourceRecordType.NS); newRecords[i] = record; } records = newRecords; } //set last used date time DateTime utcNow = DateTime.UtcNow; foreach (DnsResourceRecord record in records) record.GetCacheRecordInfo().LastUsedOn = utcNow; //set records bool added = true; entries.AddOrUpdate(type, records, delegate (DnsResourceRecordType key, IReadOnlyList existingRecords) { added = false; return records; }); if (serveStale && !isFailureRecord) { //remove stale CNAME entry only when serve stale is enabled //making sure current record is not a failure record causing removal of useful stale CNAME record switch (type) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.SOA: case DnsResourceRecordType.NS: case DnsResourceRecordType.DS: //do nothing break; default: //remove stale CNAME entry since current new entry type overlaps any existing CNAME entry in cache //keeping both entries will create issue with serve stale implementation since stale CNAME entry will be always returned if (entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList existingCNAMERecords)) { if ((existingCNAMERecords.Count > 0) && (existingCNAMERecords[0].RDATA is DnsCNAMERecordData) && existingCNAMERecords[0].IsStale) { //delete CNAME entry only when it contains stale DnsCNAMERecord RDATA and not special cache records entries.TryRemove(DnsResourceRecordType.CNAME, out _); } } break; } } return added; } public int RemoveExpiredRecords(bool serveStale) { int removedEntries = 0; if (_ecsEntries is not null) { foreach (KeyValuePair>> ecsEntry in _ecsEntries) { foreach (KeyValuePair> entry in ecsEntry.Value) { if (DnsResourceRecord.IsRRSetExpired(entry.Value, serveStale)) { if (ecsEntry.Value.TryRemove(entry.Key, out _)) //RR Set is expired; remove entry removedEntries++; } } if (ecsEntry.Value.IsEmpty) _ecsEntries.TryRemove(ecsEntry.Key, out _); } } foreach (KeyValuePair> entry in _entries) { if (DnsResourceRecord.IsRRSetExpired(entry.Value, serveStale)) { if (_entries.TryRemove(entry.Key, out _)) //RR Set is expired; remove entry removedEntries++; } } return removedEntries; } public int RemoveLeastUsedRecords(DateTime cutoff) { int removedEntries = 0; if (_ecsEntries is not null) { foreach (KeyValuePair>> ecsEntry in _ecsEntries) { foreach (KeyValuePair> entry in ecsEntry.Value) { if ((entry.Value.Count == 0) || (entry.Value[0].GetCacheRecordInfo().LastUsedOn < cutoff)) { if (ecsEntry.Value.TryRemove(entry.Key, out _)) //RR Set was last used before cutoff; remove entry removedEntries++; } } if (ecsEntry.Value.IsEmpty) _ecsEntries.TryRemove(ecsEntry.Key, out _); } } foreach (KeyValuePair> entry in _entries) { if ((entry.Value.Count == 0) || (entry.Value[0].GetCacheRecordInfo().LastUsedOn < cutoff)) { if (_entries.TryRemove(entry.Key, out _)) //RR Set was last used before cutoff; remove entry removedEntries++; } } return removedEntries; } public int DeleteEDnsClientSubnetData() { if (_ecsEntries is null) return 0; int count = 0; foreach (KeyValuePair>> ecsEntry in _ecsEntries) count += ecsEntry.Value.Count; _ecsEntries = null; return count; } public IReadOnlyList QueryRecords(DnsResourceRecordType type, bool serveStale, bool skipSpecialCacheRecord, NetworkAddress eDnsClientSubnet, bool advancedForwardingClientSubnet) { ConcurrentDictionary> entries; if (eDnsClientSubnet is null) { entries = _entries; } else { if (_ecsEntries is null) return Array.Empty(); if (advancedForwardingClientSubnet) { if (!_ecsEntries.TryGetValue(eDnsClientSubnet, out entries)) return Array.Empty(); } else { NetworkAddress selectedNetwork = null; entries = null; foreach (KeyValuePair>> ecsEntry in _ecsEntries) { NetworkAddress cacheSubnet = ecsEntry.Key; if (cacheSubnet.PrefixLength > eDnsClientSubnet.PrefixLength) continue; if (cacheSubnet.Equals(eDnsClientSubnet) || cacheSubnet.Contains(eDnsClientSubnet.Address)) { if ((selectedNetwork is null) || (cacheSubnet.PrefixLength < selectedNetwork.PrefixLength)) { selectedNetwork = cacheSubnet; entries = ecsEntry.Value; } } } if (entries is null) return Array.Empty(); } } switch (type) { case DnsResourceRecordType.DS: { //since some zones have CNAME at apex so no CNAME lookup for DS queries! if (entries.TryGetValue(type, out IReadOnlyList existingRecords)) return ValidateRRSet(type, existingRecords, serveStale, skipSpecialCacheRecord); } break; case DnsResourceRecordType.SOA: case DnsResourceRecordType.DNSKEY: { //since some zones have CNAME at apex! if (entries.TryGetValue(type, out IReadOnlyList existingRecords)) return ValidateRRSet(type, existingRecords, serveStale, skipSpecialCacheRecord); if (entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList existingCNAMERecords)) { IReadOnlyList rrset = ValidateRRSet(type, existingCNAMERecords, serveStale, skipSpecialCacheRecord); if (rrset.Count > 0) { if ((type == DnsResourceRecordType.CNAME) || (rrset[0].RDATA is DnsCNAMERecordData)) return rrset; } } } break; case DnsResourceRecordType.ANY: List anyRecords = new List(entries.Count * 2); foreach (KeyValuePair> entry in entries) { if (entry.Key == DnsResourceRecordType.DS) continue; anyRecords.AddRange(ValidateRRSet(type, entry.Value, serveStale, true)); } return anyRecords; default: { if (entries.TryGetValue(DnsResourceRecordType.CNAME, out IReadOnlyList existingCNAMERecords)) { IReadOnlyList rrset = ValidateRRSet(type, existingCNAMERecords, serveStale, skipSpecialCacheRecord); if (rrset.Count > 0) { if ((type == DnsResourceRecordType.CNAME) || (rrset[0].RDATA is DnsCNAMERecordData)) return rrset; } } if (entries.TryGetValue(type, out IReadOnlyList existingRecords)) return ValidateRRSet(type, existingRecords, serveStale, skipSpecialCacheRecord); } break; } return Array.Empty(); } public override void ListAllRecords(List records) { if (_ecsEntries is not null) { foreach (KeyValuePair>> ecsEntry in _ecsEntries) { foreach (KeyValuePair> entry in ecsEntry.Value) records.AddRange(entry.Value); } } base.ListAllRecords(records); } public override bool ContainsNameServerRecords() { if (!_entries.TryGetValue(DnsResourceRecordType.NS, out IReadOnlyList records)) { if ((_name.Length > 0) || !_entries.TryGetValue(DnsResourceRecordType.CHILD_NS, out records)) //root zone case return false; } foreach (DnsResourceRecord record in records) { if (record.IsStale) continue; if (record.RDATA is DnsNSRecordData) return true; } return false; } public void WriteTo(BinaryWriter bW) { bW.Write((byte)1); //version //cache zone info bW.Write(_name); //write all cache records WriteEntriesTo(_entries, bW); //write all ECS cache records if (_ecsEntries is null) { bW.Write(0); } else { bW.Write(_ecsEntries.Count); foreach (KeyValuePair>> ecsEntry in _ecsEntries) { ecsEntry.Key.WriteTo(bW); WriteEntriesTo(ecsEntry.Value, bW); } } } #endregion #region properties public override bool IsEmpty { get { if (_ecsEntries is null) return _entries.IsEmpty; return _ecsEntries.IsEmpty && _entries.IsEmpty; } } public int TotalEntries { get { if (_ecsEntries is null) return _entries.Count; int count = _entries.Count; foreach (KeyValuePair>> ecsEntry in _ecsEntries) count += ecsEntry.Value.Count; return count; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/CatalogSubDomainZone.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class CatalogSubDomainZone : ForwarderSubDomainZone { #region constructor public CatalogSubDomainZone(CatalogZone catalogZone, string name) : base(catalogZone, name) { } #endregion #region public public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { return []; //catalog zone is not queriable } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/CatalogZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Threading; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class CatalogZone : ForwarderZone { #region variables readonly Dictionary _membersIndex = new Dictionary(); readonly ReaderWriterLockSlim _membersIndexLock = new ReaderWriterLockSlim(); #endregion #region constructor public CatalogZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { } public CatalogZone(DnsServer dnsServer, string name) : base(dnsServer, name) { } #endregion #region IDisposable protected override void Dispose(bool disposing) { try { _membersIndexLock.Dispose(); } finally { base.Dispose(disposing); } } #endregion #region internal internal override void InitZone() { //init catalog zone with dummy SOA and NS records DnsSOARecordData soa = new DnsSOARecordData("invalid", "invalid", 1, 300, 60, 604800, 900); DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _entries[DnsResourceRecordType.SOA] = [soaRecord]; _entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 0, new DnsNSRecordData("invalid"))]; } internal void InitZoneProperties() { //set catalog zone version record _dnsServer.AuthZoneManager.SetRecord(_name, new DnsResourceRecord("version." + _name, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData("2"))); //init catalog global properties QueryAccess = AuthZoneQueryAccess.Allow; ZoneTransfer = AuthZoneTransfer.Deny; } internal void BuildMembersIndex() { foreach (KeyValuePair memberEntry in EnumerateCatalogMemberZones(_dnsServer)) _membersIndex.TryAdd(memberEntry.Key.ToLowerInvariant(), memberEntry.Value); } #endregion #region catalog public void AddMemberZone(string memberZoneName, AuthZoneType zoneType) { memberZoneName = memberZoneName.ToLowerInvariant(); _membersIndexLock.EnterWriteLock(); try { if (_membersIndex.TryGetValue(memberZoneName, out _)) { if (_membersIndex.Remove(memberZoneName, out string removedMemberZoneDomain)) { foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, removedMemberZoneDomain, true)) _dnsServer.AuthZoneManager.DeleteRecord(_name, record); } } string memberZoneDomain = GetDomainWithLabel("zones." + _name); DateTime utcNow = DateTime.UtcNow; DnsResourceRecord ptrRecord = new DnsResourceRecord(memberZoneDomain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(memberZoneName)); ptrRecord.GetAuthGenericRecordInfo().LastModified = utcNow; DnsResourceRecord txtRecord = new DnsResourceRecord("zone-type.ext." + memberZoneDomain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(zoneType.ToString().ToLowerInvariant())); txtRecord.GetAuthGenericRecordInfo().LastModified = utcNow; _dnsServer.AuthZoneManager.AddRecord(_name, ptrRecord); _dnsServer.AuthZoneManager.AddRecord(_name, txtRecord); _membersIndex[memberZoneName] = memberZoneDomain; } finally { _membersIndexLock.ExitWriteLock(); } } public bool RemoveMemberZone(string memberZoneName) { memberZoneName = memberZoneName.ToLowerInvariant(); _membersIndexLock.EnterWriteLock(); try { if (_membersIndex.Remove(memberZoneName, out string removedMemberZoneDomain)) { foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, removedMemberZoneDomain, true)) _dnsServer.AuthZoneManager.DeleteRecord(_name, record); return true; } return false; } finally { _membersIndexLock.ExitWriteLock(); } } public void ChangeMemberZoneOwnership(string memberZoneName, string newCatalogZoneName) { string memberZoneDomain = GetMemberZoneDomain(memberZoneName); string domain = "coo." + memberZoneDomain; DateTime utcNow = DateTime.UtcNow; uint soaExpiry = GetZoneSoaExpire(); //add COO record with expiry DnsResourceRecord cooRecord = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(newCatalogZoneName)); GenericRecordInfo cooRecordInfo = cooRecord.GetAuthGenericRecordInfo(); cooRecordInfo.LastModified = utcNow; cooRecordInfo.ExpiryTtl = soaExpiry; _dnsServer.AuthZoneManager.SetRecord(_name, cooRecord); //set expiry for other member zone records foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, memberZoneDomain, true)) { GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); recordInfo.LastModified = utcNow; recordInfo.ExpiryTtl = soaExpiry; } } public IReadOnlyCollection GetAllMemberZoneNames() { _membersIndexLock.EnterReadLock(); try { return _membersIndex.Keys.ToArray(); } finally { _membersIndexLock.ExitReadLock(); } } public AuthZoneType GetZoneTypeProperty(string memberZoneName) { string domain = "zone-type.ext." + GetMemberZoneDomain(memberZoneName); IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT); if (records.Count > 0) return Enum.Parse((records[0].RDATA as DnsTXTRecordData).GetText(), true); return AuthZoneType.Primary; } public void SetAllowQueryProperty(IReadOnlyCollection acl = null, string memberZoneName = null) { string domain = "allow-query.ext." + GetMemberZoneDomain(memberZoneName); if (acl is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.APL); } else { DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.APL, DnsClass.IN, 0, NetworkAccessControl.ConvertToAPLRecordData(acl)); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _dnsServer.AuthZoneManager.SetRecord(_name, record); } } public void SetAllowTransferProperty(IReadOnlyCollection acl = null, string memberZoneName = null) { string domain = "allow-transfer.ext." + GetMemberZoneDomain(memberZoneName); if (acl is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.APL); } else { DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.APL, DnsClass.IN, 0, NetworkAccessControl.ConvertToAPLRecordData(acl)); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _dnsServer.AuthZoneManager.SetRecord(_name, record); } } public void SetZoneTransferTsigKeyNamesProperty(IReadOnlySet tsigKeyNames = null, string memberZoneName = null) { string domain = "transfer-tsig-key-names.ext." + GetMemberZoneDomain(memberZoneName); if (tsigKeyNames is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.PTR); } else { DnsResourceRecord[] records = new DnsResourceRecord[tsigKeyNames.Count]; int i = 0; foreach (string entry in tsigKeyNames) { DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(entry)); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; records[i++] = record; } _dnsServer.AuthZoneManager.SetRecords(_name, records); } } public void SetPrimaryAddressesProperty(IReadOnlyList primaryServerAddresses = null, string memberZoneName = null) { string domain = "primary-addresses.ext." + GetMemberZoneDomain(memberZoneName); if (primaryServerAddresses is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT); } else { IReadOnlyList charStrings = primaryServerAddresses.Convert(delegate (NameServerAddress nameServer) { return nameServer.ToString(); }); DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(charStrings)); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _dnsServer.AuthZoneManager.SetRecord(_name, record); } } public void SetPrimaryZoneTransferProtocolProperty(DnsTransportProtocol? zoneTransferProtocol = null, string memberZoneName = null) { string domain = "primary-transfer-protocol.ext." + GetMemberZoneDomain(memberZoneName); if (zoneTransferProtocol is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT); } else { DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(zoneTransferProtocol.ToString())); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _dnsServer.AuthZoneManager.SetRecord(_name, record); } } public void SetPrimaryZoneTransferTsigKeyNameProperty(string tsigKeyName = null, string memberZoneName = null) { string domain = "primary-transfer-tsig-key-name.ext." + GetMemberZoneDomain(memberZoneName); if (tsigKeyName is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.PTR); } else { DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(tsigKeyName)); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _dnsServer.AuthZoneManager.SetRecord(_name, record); } } public void SetZoneMdValidationProperty(bool? validateZone = null, string memberZoneName = null) { string domain = "zonemd-validation.ext." + GetMemberZoneDomain(memberZoneName); if (validateZone is null) { _dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT); } else { DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(validateZone.ToString())); record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _dnsServer.AuthZoneManager.SetRecord(_name, record); } } private string GetMemberZoneDomain(string memberZoneName = null) { if (memberZoneName is null) { return _name; } else { memberZoneName = memberZoneName.ToLowerInvariant(); _membersIndexLock.EnterReadLock(); try { if (!_membersIndex.TryGetValue(memberZoneName, out string memberZoneDomain)) throw new DnsServerException("Failed to find '" + memberZoneName + "' member zone entry in '" + ToString() + "' Catalog zone: member zone does not exists."); return memberZoneDomain; } finally { _membersIndexLock.ExitReadLock(); } } } private string GetDomainWithLabel(string domain) { Span buffer = stackalloc byte[8]; int i = 0; do { RandomNumberGenerator.Fill(buffer); string label = Base32.ToBase32HexString(buffer, true).ToLowerInvariant(); string domainWithLabel = label + "." + domain; if (_dnsServer.AuthZoneManager.NameExists(_name, domainWithLabel)) continue; return domainWithLabel; } while (++i < 10); throw new DnsServerException("Failed to generate unique label for the given domain name '" + domain + "'. Please try again."); } #endregion #region public public override string GetZoneTypeName() { return "Catalog"; } public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { switch (type) { case DnsResourceRecordType.SOA: if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Invalid SOA record."); DnsResourceRecord newSoaRecord = records[0]; DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData; //reset fixed record values DnsSOARecordData modifiedSoa = new DnsSOARecordData("invalid", "invalid", newSoa.Serial, newSoa.Refresh, newSoa.Retry, newSoa.Expire, newSoa.Minimum); DnsResourceRecord modifiedSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, modifiedSoa) { Tag = newSoaRecord.Tag }; base.SetRecords(type, [modifiedSoaRecord]); break; default: throw new InvalidOperationException("Cannot set records in Catalog zone."); } } public override bool AddRecord(DnsResourceRecord record) { throw new InvalidOperationException("Cannot add record in Catalog zone."); } public override bool DeleteRecords(DnsResourceRecordType type) { throw new InvalidOperationException("Cannot delete record in Catalog zone."); } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) { throw new InvalidOperationException("Cannot delete records in Catalog zone."); } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { throw new InvalidOperationException("Cannot update record in Catalog zone."); } public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { if (type == DnsResourceRecordType.SOA) return base.QueryRecords(type, dnssecOk); //allow SOA for zone transfer to work with bind return []; //catalog zone is not queriable } #endregion #region properties public override string CatalogZoneName { get { return base.CatalogZoneName; } set { throw new InvalidOperationException(); } } public override bool OverrideCatalogQueryAccess { get { return base.OverrideCatalogQueryAccess; } set { throw new InvalidOperationException(); } } public override bool OverrideCatalogZoneTransfer { get { return base.OverrideCatalogZoneTransfer; } set { throw new InvalidOperationException(); } } public override bool OverrideCatalogNotify { get { return base.OverrideCatalogNotify; } set { throw new InvalidOperationException(); } } public override AuthZoneUpdate Update { get { return base.Update; } set { throw new InvalidOperationException(); } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/ForwarderSubDomainZone.cs ================================================ /* 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 System; using System.Collections.Generic; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class ForwarderSubDomainZone : SubDomainZone { #region variables readonly ForwarderZone _forwarderZone; #endregion #region constructor public ForwarderSubDomainZone(ForwarderZone forwarderZone, string name) : base(forwarderZone, name) { _forwarderZone = forwarderZone; } #endregion #region public public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { switch (type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot set SOA record on sub domain."); case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot set DNSSEC records."); default: if (records[0].OriginalTtlValue > _forwarderZone.GetZoneSoaExpire()) throw new DnsServerException("Cannot set records: TTL cannot be greater than SOA EXPIRE."); if (!TrySetRecords(type, records, out IReadOnlyList deletedRecords)) throw new DnsServerException("Cannot set records. Please try again."); _forwarderZone.CommitAndIncrementSerial(deletedRecords, records); _forwarderZone.TriggerNotify(); break; } } public override bool AddRecord(DnsResourceRecord record) { switch (record.Type) { case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot add DNSSEC record."); default: if (record.OriginalTtlValue > _forwarderZone.GetZoneSoaExpire()) throw new DnsServerException("Cannot add record: TTL cannot be greater than SOA EXPIRE."); AddRecord(record, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); if (addedRecords.Count > 0) { _forwarderZone.CommitAndIncrementSerial(deletedRecords, addedRecords); _forwarderZone.TriggerNotify(); return true; } return false; } } public override bool DeleteRecords(DnsResourceRecordType type) { if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) { _forwarderZone.CommitAndIncrementSerial(removedRecords); _forwarderZone.TriggerNotify(); return true; } return false; } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata) { if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord)) { _forwarderZone.CommitAndIncrementSerial([deletedRecord]); _forwarderZone.TriggerNotify(); return true; } return false; } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { switch (oldRecord.Type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record."); default: if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); if (newRecord.OriginalTtlValue > _forwarderZone.GetZoneSoaExpire()) throw new DnsServerException("Cannot update record: TTL cannot be greater than SOA EXPIRE."); if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord)) throw new InvalidOperationException("Cannot update record: the record does not exists to be updated."); AddRecord(newRecord, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); List allDeletedRecords = new List(deletedRecords.Count + 1); allDeletedRecords.Add(deletedRecord); allDeletedRecords.AddRange(deletedRecords); _forwarderZone.CommitAndIncrementSerial(allDeletedRecords, addedRecords); _forwarderZone.TriggerNotify(); break; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/ForwarderZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class ForwarderZone : ApexZone { #region constructor public ForwarderZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { InitNotify(); InitRecordExpiry(); } public ForwarderZone(DnsServer dnsServer, string name) : base(dnsServer, name) { InitZone(); InitNotify(); InitRecordExpiry(); } public ForwarderZone(DnsServer dnsServer, string name, DnsTransportProtocol forwarderProtocol, string forwarder, bool dnssecValidation, DnsForwarderRecordProxyType proxyType, string proxyAddress, ushort proxyPort, string proxyUsername, string proxyPassword, string fwdRecordComments) : base(dnsServer, name) { DnsResourceRecord fwdRecord = new DnsResourceRecord(name, DnsResourceRecordType.FWD, DnsClass.IN, 0, new DnsForwarderRecordData(forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, 0)); if (!string.IsNullOrEmpty(fwdRecordComments)) fwdRecord.GetAuthGenericRecordInfo().Comments = fwdRecordComments; fwdRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _entries[DnsResourceRecordType.FWD] = [fwdRecord]; InitZone(); InitNotify(); InitRecordExpiry(); } #endregion #region internal internal virtual void InitZone() { //init forwarder zone with dummy SOA record DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, "invalid", 1, 900, 300, 604800, 900); DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _entries[DnsResourceRecordType.SOA] = [soaRecord]; } #endregion #region public public override string GetZoneTypeName() { return "Conditional Forwarder"; } public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { switch (type) { case DnsResourceRecordType.CNAME: throw new InvalidOperationException("Cannot set CNAME record at zone apex."); case DnsResourceRecordType.SOA: if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Invalid SOA record."); DnsResourceRecord newSoaRecord = records[0]; DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData; if (newSoaRecord.OriginalTtlValue > newSoa.Expire) throw new DnsServerException("Cannot set record: TTL cannot be greater than SOA EXPIRE."); if (newSoa.Retry > newSoa.Refresh) throw new DnsServerException("Cannot set record: SOA RETRY cannot be greater than SOA REFRESH."); if (newSoa.Refresh > newSoa.Expire) throw new DnsServerException("Cannot set record: SOA REFRESH cannot be greater than SOA EXPIRE."); { //reset fixed record values DnsSOARecordData modifiedSoa = new DnsSOARecordData(newSoa.PrimaryNameServer, "invalid", newSoa.Serial, newSoa.Refresh, newSoa.Retry, newSoa.Expire, newSoa.Minimum); newSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, modifiedSoa) { Tag = newSoaRecord.Tag }; records = [newSoaRecord]; } //remove any record info except serial date scheme and comments bool useSoaSerialDateScheme; string comments; { SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo(); useSoaSerialDateScheme = recordInfo.UseSoaSerialDateScheme; comments = recordInfo.Comments; } newSoaRecord.Tag = null; //remove old record info { SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo(); recordInfo.UseSoaSerialDateScheme = useSoaSerialDateScheme; recordInfo.Comments = comments; recordInfo.LastModified = DateTime.UtcNow; } //setting new SOA CommitAndIncrementSerial(null, records); TriggerNotify(); break; case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot set DNSSEC records."); default: if (records[0].OriginalTtlValue > GetZoneSoaExpire()) throw new DnsServerException("Cannot set records: TTL cannot be greater than SOA EXPIRE."); if (!TrySetRecords(type, records, out IReadOnlyList deletedRecords)) throw new DnsServerException("Cannot set records. Please try again."); CommitAndIncrementSerial(deletedRecords, records); TriggerNotify(); break; } } public override bool AddRecord(DnsResourceRecord record) { switch (record.Type) { case DnsResourceRecordType.DS: case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot set DNSSEC records."); default: if (record.OriginalTtlValue > GetZoneSoaExpire()) throw new DnsServerException("Cannot add record: TTL cannot be greater than SOA EXPIRE."); AddRecord(record, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); if (addedRecords.Count > 0) { CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); return true; } return false; } } public override bool DeleteRecords(DnsResourceRecordType type) { switch (type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot delete SOA record."); default: if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) { CommitAndIncrementSerial(removedRecords); TriggerNotify(); return true; } return false; } } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata) { switch (type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot delete SOA record."); default: if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord)) { CommitAndIncrementSerial([deletedRecord]); TriggerNotify(); return true; } return false; } } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { switch (oldRecord.Type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record"); default: if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); if (newRecord.OriginalTtlValue > GetZoneSoaExpire()) throw new DnsServerException("Cannot update record: TTL cannot be greater than SOA EXPIRE."); if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord)) throw new DnsServerException("Cannot update record: the record does not exists to be updated."); AddRecord(newRecord, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); List allDeletedRecords = new List(deletedRecords.Count + 1); allDeletedRecords.Add(deletedRecord); allDeletedRecords.AddRange(deletedRecords); CommitAndIncrementSerial(allDeletedRecords, addedRecords); TriggerNotify(); break; } } public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { if (this is CatalogZone) return base.QueryRecords(type, dnssecOk); if (type == DnsResourceRecordType.SOA) return []; //forwarder zone is not authoritative and contains dummy SOA record return base.QueryRecords(type, dnssecOk); } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { if (base.Disabled == value) return; base.Disabled = value; //set value early to be able to use it for notify if (value) DisableNotifyTimer(); else TriggerNotify(); } } public override AuthZoneQueryAccess QueryAccess { get { return base.QueryAccess; } set { switch (value) { case AuthZoneQueryAccess.AllowOnlyZoneNameServers: case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Query Access option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(QueryAccess)); } base.QueryAccess = value; } } public override AuthZoneTransfer ZoneTransfer { get { return base.ZoneTransfer; } set { switch (value) { case AuthZoneTransfer.AllowOnlyZoneNameServers: case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Zone Transfer option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(ZoneTransfer)); } base.ZoneTransfer = value; } } public override AuthZoneNotify Notify { get { return base.Notify; } set { switch (value) { case AuthZoneNotify.ZoneNameServers: case AuthZoneNotify.BothZoneAndSpecifiedNameServers: throw new ArgumentException("The Notify option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Notify)); case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones: if (this is CatalogZone) break; throw new ArgumentException("The Notify option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Notify)); } base.Notify = value; } } public override AuthZoneUpdate Update { get { return base.Update; } set { switch (value) { case AuthZoneUpdate.AllowOnlyZoneNameServers: case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Dynamic Updates option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Update)); } base.Update = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/PrimarySubDomainZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class PrimarySubDomainZone : SubDomainZone { #region variables readonly PrimaryZone _primaryZone; #endregion #region constructor public PrimarySubDomainZone(PrimaryZone primaryZone, string name) : base(primaryZone, name) { _primaryZone = primaryZone; } #endregion #region DNSSEC internal override IReadOnlyList SignRRSet(IReadOnlyList records) { return _primaryZone.SignRRSet(records); } #endregion #region public public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) { switch (type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.APP: throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones."); default: foreach (DnsResourceRecord record in records) { if (record.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot set records: disabling records in a signed zones is not supported."); } break; } } switch (type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot set SOA record on sub domain."); case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot set DNSSEC records."); case DnsResourceRecordType.FWD: throw new DnsServerException("The record type is not supported by primary zones."); default: if (records[0].OriginalTtlValue > _primaryZone.GetZoneSoaExpire()) throw new DnsServerException("Cannot set records: TTL cannot be greater than SOA EXPIRE."); if (!TrySetRecords(type, records, out IReadOnlyList deletedRecords)) throw new DnsServerException("Cannot set records. Please try again."); _primaryZone.CommitAndIncrementSerial(deletedRecords, records); if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) _primaryZone.UpdateDnssecRecordsFor(this, type); _primaryZone.TriggerNotify(); break; } } public override bool AddRecord(DnsResourceRecord record) { if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) { switch (record.Type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.APP: throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones."); default: if (record.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot add record: disabling records in a signed zones is not supported."); break; } } switch (record.Type) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot add DNSSEC record."); case DnsResourceRecordType.FWD: throw new DnsServerException("The record type is not supported by primary zones."); default: if (record.OriginalTtlValue > _primaryZone.GetZoneSoaExpire()) throw new DnsServerException("Cannot add record: TTL cannot be greater than SOA EXPIRE."); AddRecord(record, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); if (addedRecords.Count > 0) { _primaryZone.CommitAndIncrementSerial(deletedRecords, addedRecords); if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) _primaryZone.UpdateDnssecRecordsFor(this, record.Type); _primaryZone.TriggerNotify(); return true; } return false; } } public override bool DeleteRecords(DnsResourceRecordType type) { switch (type) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot delete DNSSEC records."); default: if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) { _primaryZone.CommitAndIncrementSerial(removedRecords); if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) _primaryZone.UpdateDnssecRecordsFor(this, type); _primaryZone.TriggerNotify(); return true; } return false; } } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata) { switch (type) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot delete DNSSEC records."); default: if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord)) { _primaryZone.CommitAndIncrementSerial([deletedRecord]); if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) _primaryZone.UpdateDnssecRecordsFor(this, type); _primaryZone.TriggerNotify(); return true; } return false; } } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { switch (oldRecord.Type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record."); case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot update DNSSEC records."); default: if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); if ((_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot update record: disabling records in a signed zones is not supported."); if (newRecord.OriginalTtlValue > _primaryZone.GetZoneSoaExpire()) throw new DnsServerException("Cannot update record: TTL cannot be greater than SOA EXPIRE."); if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord)) throw new InvalidOperationException("Cannot update record: the record does not exists to be updated."); AddRecord(newRecord, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); List allDeletedRecords = new List(deletedRecords.Count + 1); allDeletedRecords.Add(deletedRecord); allDeletedRecords.AddRange(deletedRecords); _primaryZone.CommitAndIncrementSerial(allDeletedRecords, addedRecords); if (_primaryZone.DnssecStatus != AuthZoneDnssecStatus.Unsigned) _primaryZone.UpdateDnssecRecordsFor(this, oldRecord.Type); _primaryZone.TriggerNotify(); break; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/PrimaryZone.cs ================================================ /* 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.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.ZoneManagers; using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { public enum AuthZoneDnssecStatus : byte { Unsigned = 0, SignedWithNSEC = 1, SignedWithNSEC3 = 2, } //DNSSEC Operational Practices, Version 2 //https://datatracker.ietf.org/doc/html/rfc6781 //DNSSEC Key Rollover Timing Considerations //https://datatracker.ietf.org/doc/html/rfc7583 class PrimaryZone : ApexZone { #region variables readonly bool _internal; Dictionary _dnssecPrivateKeys; const uint DNSSEC_SIGNATURE_INCEPTION_OFFSET = 60 * 60; Timer _dnssecTimer; const int DNSSEC_TIMER_INITIAL_INTERVAL = 30000; internal const int DNSSEC_TIMER_PERIODIC_INTERVAL = 900000; DateTime _lastSignatureRefreshCheckedOn; readonly object _dnssecUpdateLock = new object(); #endregion #region constructor public PrimaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { IReadOnlyCollection dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys; if (dnssecPrivateKeys is not null) { _dnssecPrivateKeys = new Dictionary(dnssecPrivateKeys.Count); foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) _dnssecPrivateKeys.Add(dnssecPrivateKey.KeyTag, dnssecPrivateKey); } InitNotify(); InitRecordExpiry(); } public PrimaryZone(DnsServer dnsServer, string name, bool @internal, bool useSoaSerialDateScheme) : base(dnsServer, name) { _internal = @internal; if (!_internal) { InitNotify(); InitRecordExpiry(); ZoneTransfer = AuthZoneTransfer.AllowOnlyZoneNameServers; Notify = AuthZoneNotify.ZoneNameServers; } string rp; if (_dnsServer.DefaultResponsiblePerson is null) rp = _name.Length == 0 ? _dnsServer.ResponsiblePerson.Address : "hostadmin@" + _name; else rp = _dnsServer.DefaultResponsiblePerson.Address; uint serial = GetNewSerial(0, 0, useSoaSerialDateScheme); DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, rp, serial, 900, 300, 604800, dnsServer.AuthZoneManager.DefaultSoaRecordTtl); DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Minimum, soa); soaRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme = useSoaSerialDateScheme; soaRecord.GetAuthSOARecordInfo().LastModified = DateTime.UtcNow; DnsResourceRecord nsRecord = new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, dnsServer.AuthZoneManager.DefaultNsRecordTtl, new DnsNSRecordData(soa.PrimaryNameServer)); nsRecord.GetAuthNSRecordInfo().LastModified = DateTime.UtcNow; _entries[DnsResourceRecordType.SOA] = [soaRecord]; _entries[DnsResourceRecordType.NS] = [nsRecord]; } internal PrimaryZone(DnsServer dnsServer, string name, DnsSOARecordData soa, DnsNSRecordData ns) : base(dnsServer, name) { _internal = true; _entries[DnsResourceRecordType.SOA] = [new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, soa.Minimum, soa)]; _entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, dnsServer.AuthZoneManager.DefaultNsRecordTtl, ns)]; } #endregion #region IDisposable bool _disposed; protected override void Dispose(bool disposing) { try { if (_disposed) return; if (disposing) { Timer dnssecTimer = _dnssecTimer; if (dnssecTimer is not null) { lock (dnssecTimer) { dnssecTimer.Dispose(); _dnssecTimer = null; } } } _disposed = true; } finally { base.Dispose(disposing); } } #endregion #region DNSSEC internal override void UpdateDnssecStatus() { base.UpdateDnssecStatus(); if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) { if (_dnssecPrivateKeys is not null) _dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private async void DnssecTimerCallback(object state) { try { List kskToReady = null; List kskToActivate = null; List kskToRetire = null; List kskToRevoke = null; List zskToActivate = null; List zskToRetire = null; List zskToRollover = null; List keysToUnpublish = null; bool saveZone = false; lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if (privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) { //KSK switch (privateKey.State) { case DnssecPrivateKeyState.Published: if (DateTime.UtcNow > privateKey.StateTransitionBy) { //long enough time for old RRsets to expire from caches if (kskToReady is null) kskToReady = new List(); kskToReady.Add(privateKey); } break; case DnssecPrivateKeyState.Ready: if (privateKey.IsRetiring) { if (kskToRetire is null) kskToRetire = new List(); kskToRetire.Add(privateKey); } else { if (kskToActivate is null) kskToActivate = new List(); kskToActivate.Add(privateKey); } break; case DnssecPrivateKeyState.Active: if (privateKey.IsRetiring) { if (kskToRetire is null) kskToRetire = new List(); kskToRetire.Add(privateKey); } break; case DnssecPrivateKeyState.Retired: //KSK needs to be revoked for RFC5011 consideration if (DateTime.UtcNow > privateKey.StateTransitionBy) { //key has been retired for sufficient time if (kskToRevoke is null) kskToRevoke = new List(); kskToRevoke.Add(privateKey); } break; case DnssecPrivateKeyState.Revoked: if (DateTime.UtcNow > privateKey.StateTransitionBy) { //key has been revoked for sufficient time if (keysToUnpublish is null) keysToUnpublish = new List(); keysToUnpublish.Add(privateKey); } break; } } else { //ZSK switch (privateKey.State) { case DnssecPrivateKeyState.Published: if (DateTime.UtcNow > privateKey.StateTransitionBy) { //long enough time old RRset to expire from caches privateKey.SetState(DnssecPrivateKeyState.Ready); if (zskToActivate is null) zskToActivate = new List(); zskToActivate.Add(privateKey); } break; case DnssecPrivateKeyState.Ready: if (zskToActivate is null) zskToActivate = new List(); zskToActivate.Add(privateKey); break; case DnssecPrivateKeyState.Active: if (privateKey.IsRetiring) { if (zskToRetire is null) zskToRetire = new List(); zskToRetire.Add(privateKey); } else { if (privateKey.IsRolloverNeeded()) { if (zskToRollover is null) zskToRollover = new List(); zskToRollover.Add(privateKey); } } break; case DnssecPrivateKeyState.Retired: if (DateTime.UtcNow > privateKey.StateTransitionBy) { //key has been retired for sufficient time if (keysToUnpublish is null) keysToUnpublish = new List(); keysToUnpublish.Add(privateKey); } break; } } } } #region KSK actions if (kskToReady is not null) { string dnsKeyTags = null; foreach (DnssecPrivateKey kskPrivateKey in kskToReady) { kskPrivateKey.SetState(DnssecPrivateKeyState.Ready); if (kskToActivate is null) kskToActivate = new List(); kskToActivate.Add(kskPrivateKey); if (dnsKeyTags is null) dnsKeyTags = kskPrivateKey.KeyTag.ToString(); else dnsKeyTags += ", " + kskPrivateKey.KeyTag.ToString(); } saveZone = true; _dnsServer.LogManager.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone are ready for changing the DS records at the parent zone: " + ToString()); } if (kskToActivate is not null) { try { IReadOnlyList kskPrivateKeys = await GetDSPublishedPrivateKeysAsync(kskToActivate); if (kskPrivateKeys.Count > 0) { string dnsKeyTags = null; foreach (DnssecPrivateKey kskPrivateKey in kskPrivateKeys) { kskPrivateKey.SetState(DnssecPrivateKeyState.Active); if (dnsKeyTags is null) dnsKeyTags = kskPrivateKey.KeyTag.ToString(); else dnsKeyTags += ", " + kskPrivateKey.KeyTag.ToString(); } saveZone = true; _dnsServer.LogManager.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were activated successfully: " + ToString()); } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } if (kskToRetire is not null) { if (await RetireKskDnsKeysAsync(kskToRetire, false)) saveZone = true; } if (kskToRevoke is not null) { RevokeKskDnsKeys(kskToRevoke); saveZone = true; } #endregion #region ZSK actions if (zskToActivate is not null) { ActivateZskDnsKeys(zskToActivate); saveZone = true; } if (zskToRetire is not null) { if (RetireZskDnsKeys(zskToRetire, false)) saveZone = true; } if (zskToRollover is not null) { foreach (DnssecPrivateKey zskPrivateKey in zskToRollover) RolloverDnsKey(zskPrivateKey.KeyTag); saveZone = true; } #endregion if (keysToUnpublish is not null) { UnpublishDnsKeys(keysToUnpublish); saveZone = true; } //re-signing task uint reSignPeriod = GetSignatureValidityPeriod() / 10; //the period when signature refresh check is done if (DateTime.UtcNow > _lastSignatureRefreshCheckedOn.AddSeconds(reSignPeriod)) { if (TryRefreshAllSignatures()) saveZone = true; _lastSignatureRefreshCheckedOn = DateTime.UtcNow; } if (saveZone) _dnsServer.AuthZoneManager.SaveZoneFile(_name); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { Timer dnssecTimer = _dnssecTimer; if (dnssecTimer is not null) { lock (dnssecTimer) { dnssecTimer.Change(DNSSEC_TIMER_PERIODIC_INTERVAL, Timeout.Infinite); } } } } public void SignZone(DnssecPrivateKey kskPrivateKey, DnssecPrivateKey zskPrivateKey, uint dnsKeyTtl, bool useNSec3, ushort iterations = 0, byte saltLength = 0) { if (kskPrivateKey.KeyType != DnssecPrivateKeyType.KeySigningKey) throw new ArgumentException("The private key must be a Key Signing Key.", nameof(kskPrivateKey)); if (zskPrivateKey.KeyType != DnssecPrivateKeyType.ZoneSigningKey) throw new ArgumentException("The private key must be a Zone Signing Key.", nameof(zskPrivateKey)); byte[] salt = null; if (useNSec3) { if (saltLength > 32) throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); if (saltLength > 0) { salt = new byte[saltLength]; RandomNumberGenerator.Fill(salt); } else { salt = []; } } SignZone([kskPrivateKey, zskPrivateKey], dnsKeyTtl, useNSec3, iterations, salt); } public void SignZone(IReadOnlyCollection dnssecPrivateKeys, uint dnsKeyTtl, bool useNSec3, ushort iterations = 0, byte[] salt = null) { //do validations if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("Cannot sign zone: the zone is already signed."); if (useNSec3) { if (iterations > 50) throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); if (salt.Length > 32) throw new ArgumentOutOfRangeException(nameof(salt), "NSEC3 salt length valid range is 0-32"); } bool foundKsk = false; bool foundZsk = false; foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) { switch (dnssecPrivateKey.KeyType) { case DnssecPrivateKeyType.KeySigningKey: foundKsk = true; break; case DnssecPrivateKeyType.ZoneSigningKey: foundZsk = true; break; } } if (!foundKsk) throw new ArgumentException("The private keys must contain at least one Key Signing Key.", nameof(dnssecPrivateKeys)); if (!foundZsk) throw new ArgumentException("The private keys must contain at least one Zone Signing Key.", nameof(dnssecPrivateKeys)); //load dnssec private keys _dnssecPrivateKeys = new Dictionary(dnssecPrivateKeys.Count); foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) _dnssecPrivateKeys.Add(dnssecPrivateKey.KeyTag, dnssecPrivateKey); //start zone signing List addedRecords = new List(); List deletedRecords = new List(); try { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); //find max record ttl in zone uint maxRecordTtl = 0; foreach (AuthZone zone in zones) { foreach (KeyValuePair> entry in zone.Entries) { IReadOnlyList rrset = entry.Value; //find min TTL uint rrsetTtl = 0; foreach (DnsResourceRecord rr in rrset) { if ((rrsetTtl == 0) || (rrsetTtl > rr.OriginalTtlValue)) rrsetTtl = rr.OriginalTtlValue; } if (rrsetTtl > maxRecordTtl) maxRecordTtl = rrsetTtl; } } //update private key state uint propagationDelay = GetPropagationDelay(); foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if (privateKey.State == DnssecPrivateKeyState.Generated) { switch (privateKey.KeyType) { case DnssecPrivateKeyType.KeySigningKey: privateKey.SetState(DnssecPrivateKeyState.Published, maxRecordTtl + propagationDelay); break; case DnssecPrivateKeyType.ZoneSigningKey: privateKey.SetState(DnssecPrivateKeyState.Ready); break; } } } //add DNSKEYs List dnsKeyRecords = new List(_dnssecPrivateKeys.Count); foreach (KeyValuePair privateKey in _dnssecPrivateKeys) dnsKeyRecords.Add(new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.Value.DnsKey)); if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords)) throw new InvalidOperationException("Failed to add DNSKEY."); addedRecords.AddRange(dnsKeyRecords); deletedRecords.AddRange(deletedDnsKeyRecords); //sign all RRSets foreach (AuthZone zone in zones) { IReadOnlyList newRRSigRecords = zone.SignAllRRSets(); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } } if (useNSec3) { EnableNSec3(zones, iterations, salt); _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3; } else { EnableNSec(zones); _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC; } //update private key state foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; switch (privateKey.KeyType) { case DnssecPrivateKeyType.ZoneSigningKey: if (privateKey.State == DnssecPrivateKeyState.Ready) privateKey.SetState(DnssecPrivateKeyState.Active); break; } } _dnssecTimer = new Timer(DnssecTimerCallback, null, DNSSEC_TIMER_INITIAL_INTERVAL, Timeout.Infinite); CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); } catch { _dnssecStatus = AuthZoneDnssecStatus.Unsigned; _dnssecPrivateKeys = null; Dictionary>> addedRecordGroups = DnsResourceRecord.GroupRecords(addedRecords); foreach (KeyValuePair>> addedRecordGroup in addedRecordGroups) { AuthZone zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, addedRecordGroup.Key); foreach (KeyValuePair> addedRecordEntry in addedRecordGroup.Value) zone.TryDeleteRecords(addedRecordEntry.Key, addedRecordEntry.Value, out _); } Dictionary>> deletedRecordGroups = DnsResourceRecord.GroupRecords(deletedRecords); foreach (KeyValuePair>> deletedRecordGroup in deletedRecordGroups) { AuthZone zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, deletedRecordGroup.Key); foreach (KeyValuePair> deletedRecordEntry in deletedRecordGroup.Value) { foreach (DnsResourceRecord deletedRecord in deletedRecordEntry.Value) zone.AddRecord(deletedRecord, out _, out _); } } throw; } } public void UnsignZone() { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("Cannot unsign zone: the is zone not signed."); List deletedRecords = new List(); IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); foreach (AuthZone zone in zones) { deletedRecords.AddRange(zone.RemoveAllDnssecRecords()); if (zone is SubDomainZone subDomainZone) { if (zone.IsEmpty) _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone else subDomainZone.AutoUpdateState(); } } Timer dnssecTimer = _dnssecTimer; if (dnssecTimer is not null) { lock (dnssecTimer) { dnssecTimer.Dispose(); _dnssecTimer = null; } } _dnssecPrivateKeys = null; _dnssecStatus = AuthZoneDnssecStatus.Unsigned; CommitAndIncrementSerial(deletedRecords); TriggerNotify(); } public void ConvertToNSec() { if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3) throw new DnsServerException("Cannot convert to NSEC: the zone must be signed with NSEC3 for conversion."); lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); DisableNSec3(zones); //since zones were removed when disabling NSEC3; get updated non empty zones list List nonEmptyZones = new List(zones.Count); foreach (AuthZone zone in zones) { if (!zone.IsEmpty) nonEmptyZones.Add(zone); } EnableNSec(nonEmptyZones); _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC; } TriggerNotify(); } public void ConvertToNSec3(ushort iterations, byte saltLength) { if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC) throw new DnsServerException("Cannot convert to NSEC3: the zone must be signed with NSEC for conversion."); if (iterations > 50) throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); if (saltLength > 32) throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); DisableNSec(zones); EnableNSec3(zones, iterations, saltLength); _dnssecStatus = AuthZoneDnssecStatus.SignedWithNSEC3; } TriggerNotify(); } public void UpdateNSec3Parameters(ushort iterations, byte saltLength) { if (_dnssecStatus != AuthZoneDnssecStatus.SignedWithNSEC3) throw new DnsServerException("Cannot update NSEC3 parameters: the zone must be signed with NSEC3 first."); if (iterations > 50) throw new ArgumentOutOfRangeException(nameof(iterations), "NSEC3 iterations valid range is 0-50"); if (saltLength > 32) throw new ArgumentOutOfRangeException(nameof(saltLength), "NSEC3 salt length valid range is 0-32"); lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); DisableNSec3(zones); //since zones were removed when disabling NSEC3; get updated non empty zones list List nonEmptyZones = new List(zones.Count); foreach (AuthZone zone in zones) { if (!zone.IsEmpty) nonEmptyZones.Add(zone); } EnableNSec3(nonEmptyZones, iterations, saltLength); } TriggerNotify(); } private void RefreshNSec() { lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); EnableNSec(zones); } } private void RefreshNSec3() { lock (_dnssecUpdateLock) { IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); //get non NSEC3 zones List nonNSec3Zones = new List(zones.Count); foreach (AuthZone zone in zones) { if (zone.HasOnlyNSec3Records()) continue; nonNSec3Zones.Add(zone); } IReadOnlyList nsec3ParamRecords = GetRecords(DnsResourceRecordType.NSEC3PARAM); DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData; EnableNSec3(nonNSec3Zones, nsec3Param.Iterations, nsec3Param.Salt); } } private void EnableNSec(IReadOnlyList zones) { List addedRecords = new List(); List deletedRecords = new List(); uint ttl = GetZoneSoaMinimum(); for (int i = 0; i < zones.Count; i++) { AuthZone zone = zones[i]; AuthZone nextZone; if (i < zones.Count - 1) nextZone = zones[i + 1]; else nextZone = zones[0]; IReadOnlyList newNSecRecords = zone.GetUpdatedNSecRRSet(nextZone.Name, ttl); if (newNSecRecords.Count > 0) { if (!zone.TrySetRecords(DnsResourceRecordType.NSEC, newNSecRecords, out IReadOnlyList deletedNSecRecords)) throw new DnsServerException("Failed to set DNSSEC records. Please try again."); addedRecords.AddRange(newNSecRecords); deletedRecords.AddRange(deletedNSecRecords); IReadOnlyList newRRSigRecords = SignRRSet(newNSecRecords); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } } } CommitAndIncrementSerial(deletedRecords, addedRecords); } private void DisableNSec(IReadOnlyList zones) { List deletedRecords = new List(); foreach (AuthZone zone in zones) deletedRecords.AddRange(zone.RemoveNSecRecordsWithRRSig()); CommitAndIncrementSerial(deletedRecords); } private void EnableNSec3(IReadOnlyList zones, ushort iterations, byte saltLength) { byte[] salt; if (saltLength > 0) { salt = new byte[saltLength]; RandomNumberGenerator.Fill(salt); } else { salt = []; } EnableNSec3(zones, iterations, salt); } private void EnableNSec3(IReadOnlyList zones, ushort iterations, byte[] salt) { List addedRecords = new List(); List deletedRecords = new List(); List partialNSec3Records = new List(zones.Count); int apexLabelCount = DnsRRSIGRecordData.GetLabelCount(_name); uint ttl = GetZoneSoaMinimum(); //list all partial NSEC3 records foreach (AuthZone zone in zones) { partialNSec3Records.Add(zone.GetPartialNSec3Record(_name, ttl, iterations, salt)); int zoneLabelCount = DnsRRSIGRecordData.GetLabelCount(zone.Name); if (zone.Name.StartsWith("*.")) zoneLabelCount++; //need to consider wildcard label for ENT detection if ((zoneLabelCount - apexLabelCount) > 1) { //empty non-terminal (ENT) may exists string currentOwnerName = zone.Name; while (true) { currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName); if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase)) break; //add partial NSEC3 record for ENT AuthZone entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object partialNSec3Records.Add(entZone.GetPartialNSec3Record(_name, ttl, iterations, salt)); } } } //sort partial NSEC3 records partialNSec3Records.Sort(delegate (DnsResourceRecord rr1, DnsResourceRecord rr2) { return string.CompareOrdinal(rr1.Name, rr2.Name); }); //deduplicate partial NSEC3 records and insert next hashed owner name to complete them List uniqueNSec3Records = new List(partialNSec3Records.Count); for (int i = 0; i < partialNSec3Records.Count; i++) { DnsResourceRecord partialNSec3Record = partialNSec3Records[i]; DnsResourceRecord nextPartialNSec3Record; if (i < partialNSec3Records.Count - 1) { nextPartialNSec3Record = partialNSec3Records[i + 1]; //check for duplicates if (partialNSec3Record.Name.Equals(nextPartialNSec3Record.Name, StringComparison.OrdinalIgnoreCase)) { //found duplicate; merge current nsec3 into next nsec3 DnsNSEC3RecordData nsec3 = partialNSec3Record.RDATA as DnsNSEC3RecordData; DnsNSEC3RecordData nextNSec3 = nextPartialNSec3Record.RDATA as DnsNSEC3RecordData; List uniqueTypes = new List(nsec3.Types.Count + nextNSec3.Types.Count); uniqueTypes.AddRange(nsec3.Types); foreach (DnsResourceRecordType type in nextNSec3.Types) { if (!uniqueTypes.Contains(type)) uniqueTypes.Add(type); } uniqueTypes.Sort(); //update the next nsec3 record and continue DnsNSEC3RecordData mergedPartialNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, Array.Empty(), uniqueTypes); partialNSec3Records[i + 1] = new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, mergedPartialNSec3); continue; } } else { //for last NSEC3, next NSEC3 is the first in list nextPartialNSec3Record = partialNSec3Records[0]; } //add NSEC3 record with next hashed owner name { DnsNSEC3RecordData partialNSec3 = partialNSec3Record.RDATA as DnsNSEC3RecordData; byte[] nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(nextPartialNSec3Record.Name); DnsNSEC3RecordData updatedNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt, nextHashedOwnerName, partialNSec3.Types); uniqueNSec3Records.Add(new DnsResourceRecord(partialNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, updatedNSec3)); } } //insert and sign NSEC3 records foreach (DnsResourceRecord uniqueNSec3Record in uniqueNSec3Records) { AuthZone zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, uniqueNSec3Record.Name); DnsResourceRecord[] newNSec3Records = new DnsResourceRecord[] { uniqueNSec3Record }; if (!zone.TrySetRecords(DnsResourceRecordType.NSEC3, newNSec3Records, out IReadOnlyList deletedNSec3Records)) throw new InvalidOperationException(); addedRecords.AddRange(newNSec3Records); deletedRecords.AddRange(deletedNSec3Records); IReadOnlyList newRRSigRecords = SignRRSet(newNSec3Records); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } } //insert and sign NSEC3PARAM record { DnsNSEC3PARAMRecordData newNSec3Param = new DnsNSEC3PARAMRecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, iterations, salt); DnsResourceRecord[] newNSec3ParamRecords = new DnsResourceRecord[] { new DnsResourceRecord(_name, DnsResourceRecordType.NSEC3PARAM, DnsClass.IN, ttl, newNSec3Param) }; if (!TrySetRecords(DnsResourceRecordType.NSEC3PARAM, newNSec3ParamRecords, out IReadOnlyList deletedNSec3ParamRecords)) throw new InvalidOperationException(); addedRecords.AddRange(newNSec3ParamRecords); deletedRecords.AddRange(deletedNSec3ParamRecords); IReadOnlyList newRRSigRecords = SignRRSet(newNSec3ParamRecords); if (newRRSigRecords.Count > 0) { AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } } CommitAndIncrementSerial(deletedRecords, addedRecords); } private void DisableNSec3(IReadOnlyList zones) { List deletedRecords = new List(); foreach (AuthZone zone in zones) { deletedRecords.AddRange(zone.RemoveNSec3RecordsWithRRSig()); if (zone is SubDomainZone subDomainZone) { if (zone.IsEmpty) _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone else subDomainZone.AutoUpdateState(); } } CommitAndIncrementSerial(deletedRecords); } public DnssecPrivateKey GenerateAndAddPrivateKey(DnssecPrivateKeyType keyType, DnssecAlgorithm algorithm, ushort rolloverDays, int keySize = -1) { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The primary zone must be signed."); int i = 0; while (i++ < 5) { DnssecPrivateKey privateKey = DnssecPrivateKey.Create(algorithm, keyType, keySize); privateKey.RolloverDays = rolloverDays; lock (_dnssecPrivateKeys) { if (_dnssecPrivateKeys.TryAdd(privateKey.KeyTag, privateKey)) return privateKey; } } throw new DnsServerException("Failed to add private key: key tag collision. Please try again."); } public void AddPrivateKey(DnssecPrivateKey privateKey) { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The primary zone must be signed."); lock (_dnssecPrivateKeys) { if (!_dnssecPrivateKeys.TryAdd(privateKey.KeyTag, privateKey)) throw new DnsServerException($"Failed to add {(privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")} private key: key tag collision. Please generate another private key and try again."); } } public DnssecPrivateKey UpdatePrivateKey(ushort keyTag, ushort rolloverDays) { lock (_dnssecPrivateKeys) { if (!_dnssecPrivateKeys.TryGetValue(keyTag, out DnssecPrivateKey privateKey)) throw new DnsServerException("Cannot update private key: no such private key was found."); privateKey.RolloverDays = rolloverDays; return privateKey; } } public void DeletePrivateKey(ushort keyTag) { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The zone must be signed."); lock (_dnssecPrivateKeys) { if (!_dnssecPrivateKeys.TryGetValue(keyTag, out DnssecPrivateKey privateKey)) throw new DnsServerException("Cannot delete private key: no such private key was found."); if (privateKey.State != DnssecPrivateKeyState.Generated) throw new DnsServerException("Cannot delete private key: only keys with Generated state can be deleted."); _dnssecPrivateKeys.Remove(keyTag); } } public void PublishAllGeneratedKeys() { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The zone must be signed."); List generatedPrivateKeys = new List(); List newDnsKeyRecords = new List(); uint dnsKeyTtl = GetDnsKeyTtl(); lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if (privateKey.State == DnssecPrivateKeyState.Generated) { generatedPrivateKeys.Add(privateKey); newDnsKeyRecords.Add(new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.DnsKey)); } } } if (generatedPrivateKeys.Count == 0) throw new DnsServerException("Cannot publish DNSKEY: no generated private keys were found."); IReadOnlyList dnsKeyRecords = _entries.AddOrUpdate(DnsResourceRecordType.DNSKEY, delegate (DnsResourceRecordType key) { return newDnsKeyRecords; }, delegate (DnsResourceRecordType key, IReadOnlyList existingRecords) { foreach (DnsResourceRecord existingRecord in existingRecords) { foreach (DnsResourceRecord newDnsKeyRecord in newDnsKeyRecords) { if (existingRecord.Equals(newDnsKeyRecord)) throw new DnsServerException("Cannot publish DNSKEY: the key is already published."); } } List dnsKeyRecords = new List(existingRecords.Count + newDnsKeyRecords.Count); dnsKeyRecords.AddRange(existingRecords); dnsKeyRecords.AddRange(newDnsKeyRecords); return dnsKeyRecords; }); //update private key state before signing uint propagationDelay = GetPropagationDelay(); foreach (DnssecPrivateKey privateKey in generatedPrivateKeys) privateKey.SetState(DnssecPrivateKeyState.Published, dnsKeyTtl + propagationDelay); List addedRecords = new List(); List deletedRecords = new List(); addedRecords.AddRange(newDnsKeyRecords); IReadOnlyList newRRSigRecords = SignRRSet(dnsKeyRecords); if (newRRSigRecords.Count > 0) { AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); } private void ActivateZskDnsKeys(IReadOnlyList zskPrivateKeys) { List addedRecords = new List(); List deletedRecords = new List(); //re-sign all records with new private keys IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); foreach (AuthZone zone in zones) { IReadOnlyList newRRSigRecords = zone.SignAllRRSets(); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } } CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); //update private key state string dnsKeyTags = null; foreach (DnssecPrivateKey privateKey in zskPrivateKeys) { privateKey.SetState(DnssecPrivateKeyState.Active); if (dnsKeyTags is null) dnsKeyTags = privateKey.KeyTag.ToString(); else dnsKeyTags += ", " + privateKey.KeyTag.ToString(); } _dnsServer.LogManager.Write("The ZSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were activated successfully: " + ToString()); } public void RolloverDnsKey(ushort keyTag) { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The zone must be signed."); DnssecPrivateKey privateKey; lock (_dnssecPrivateKeys) { if (!_dnssecPrivateKeys.TryGetValue(keyTag, out privateKey)) throw new DnsServerException("Cannot rollover private key: no such private key was found."); } switch (privateKey.State) { case DnssecPrivateKeyState.Ready: case DnssecPrivateKeyState.Active: if (privateKey.IsRetiring) throw new DnsServerException("Cannot rollover private key: the private key is already set to retire."); break; default: throw new DnsServerException("Cannot rollover private key: the private key state must be Ready or Active to be able to rollover."); } switch (privateKey.Algorithm) { case DnssecAlgorithm.RSAMD5: case DnssecAlgorithm.RSASHA1: case DnssecAlgorithm.RSASHA1_NSEC3_SHA1: case DnssecAlgorithm.RSASHA256: case DnssecAlgorithm.RSASHA512: GenerateAndAddPrivateKey(privateKey.KeyType, privateKey.Algorithm, privateKey.RolloverDays, (privateKey as DnssecRsaPrivateKey).KeySize); break; case DnssecAlgorithm.ECDSAP256SHA256: case DnssecAlgorithm.ECDSAP384SHA384: case DnssecAlgorithm.ED25519: case DnssecAlgorithm.ED448: GenerateAndAddPrivateKey(privateKey.KeyType, privateKey.Algorithm, privateKey.RolloverDays); break; default: throw new NotSupportedException("DNSSEC algorithm is not supported: " + privateKey.Algorithm.ToString()); } PublishAllGeneratedKeys(); privateKey.SetToRetire(); } public async Task RetireDnsKeyAsync(ushort keyTag) { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The zone must be signed."); DnssecPrivateKey privateKeyToRetire; lock (_dnssecPrivateKeys) { if (!_dnssecPrivateKeys.TryGetValue(keyTag, out privateKeyToRetire)) throw new DnsServerException("Cannot retire private key: no such private key was found."); } switch (privateKeyToRetire.KeyType) { case DnssecPrivateKeyType.KeySigningKey: switch (privateKeyToRetire.State) { case DnssecPrivateKeyState.Ready: case DnssecPrivateKeyState.Active: if (!await RetireKskDnsKeysAsync([privateKeyToRetire], true)) throw new DnsServerException("Cannot retire private key: no successor key was found to safely retire the key."); break; default: throw new DnsServerException("Cannot retire private key: the KSK private key state must be Ready or Active to be able to retire."); } break; case DnssecPrivateKeyType.ZoneSigningKey: switch (privateKeyToRetire.State) { case DnssecPrivateKeyState.Active: if (!RetireZskDnsKeys(new DnssecPrivateKey[] { privateKeyToRetire }, true)) throw new DnsServerException("Cannot retire private key: no successor key was found to safely retire the key."); break; default: throw new DnsServerException("Cannot retire private key: the ZSK private key state must be Active to be able to retire."); } break; default: throw new InvalidOperationException(); } } private async Task RetireKskDnsKeysAsync(IReadOnlyList kskPrivateKeys, bool ignoreAlgorithm) { string dnsKeyTags = null; uint dsTtl = 0; uint parentSidePropagationDelay = 0; foreach (DnssecPrivateKey kskPrivateKey in kskPrivateKeys) { bool isSafeToRetire = false; lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if ((privateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (privateKey.KeyTag != kskPrivateKey.KeyTag) && !privateKey.IsRetiring) { if (ignoreAlgorithm) { //manual retire case if (privateKey.Algorithm != kskPrivateKey.Algorithm) { //check if the sucessor ksk has a matching zsk bool foundMatchingZsk = false; foreach (KeyValuePair zskPrivateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey zskPrivateKey = zskPrivateKeyEntry.Value; if ((zskPrivateKey.KeyType == DnssecPrivateKeyType.ZoneSigningKey) && (zskPrivateKey.Algorithm == privateKey.Algorithm) && (zskPrivateKey.State == DnssecPrivateKeyState.Active) && !zskPrivateKey.IsRetiring) { foundMatchingZsk = true; break; } } if (!foundMatchingZsk) continue; } } else { //rollover case if (privateKey.Algorithm != kskPrivateKey.Algorithm) continue; } if (privateKey.State == DnssecPrivateKeyState.Active) { isSafeToRetire = true; break; } if ((privateKey.State == DnssecPrivateKeyState.Ready) && (kskPrivateKey.State == DnssecPrivateKeyState.Ready)) { isSafeToRetire = true; break; } } } } if (isSafeToRetire) { if (dsTtl == 0) dsTtl = await GetDSTtlAsync(); if (parentSidePropagationDelay == 0) parentSidePropagationDelay = await GetParentSidePropagationDelayAsync(); kskPrivateKey.SetState(DnssecPrivateKeyState.Retired, dsTtl + parentSidePropagationDelay); if (dnsKeyTags is null) dnsKeyTags = kskPrivateKey.KeyTag.ToString(); else dnsKeyTags += ", " + kskPrivateKey.KeyTag.ToString(); } } if (dnsKeyTags is not null) { _dnsServer.LogManager.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were retired successfully: " + ToString()); return true; } return false; } private bool RetireZskDnsKeys(IReadOnlyList zskPrivateKeys, bool ignoreAlgorithm) { string dnsKeyTags = null; List zskToDeactivate = null; uint maxRRSigTtl = 0; uint propagationDelay = 0; foreach (DnssecPrivateKey zskPrivateKey in zskPrivateKeys) { bool isSafeToRetire = false; lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if ((privateKey.KeyType == DnssecPrivateKeyType.ZoneSigningKey) && (privateKey.KeyTag != zskPrivateKey.KeyTag) && (privateKey.State == DnssecPrivateKeyState.Active) && !privateKey.IsRetiring) { if (ignoreAlgorithm) { //manual retire case if (privateKey.Algorithm != zskPrivateKey.Algorithm) { //check if the sucessor zsk has a matching ksk bool foundMatchingKsk = false; foreach (KeyValuePair kskPrivateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey kskPrivateKey = kskPrivateKeyEntry.Value; if ((kskPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (kskPrivateKey.Algorithm == privateKey.Algorithm) && ((kskPrivateKey.State == DnssecPrivateKeyState.Ready) || (kskPrivateKey.State == DnssecPrivateKeyState.Active)) && !kskPrivateKey.IsRetiring) { foundMatchingKsk = true; break; } } if (!foundMatchingKsk) continue; } } else { //rollover case if (privateKey.Algorithm != zskPrivateKey.Algorithm) continue; } isSafeToRetire = true; break; } } } if (isSafeToRetire) { if (maxRRSigTtl == 0) maxRRSigTtl = GetMaxRRSigTtl(); if (propagationDelay == 0) propagationDelay = GetPropagationDelay(); zskPrivateKey.SetState(DnssecPrivateKeyState.Retired, maxRRSigTtl + propagationDelay); if (zskToDeactivate is null) zskToDeactivate = new List(); zskToDeactivate.Add(zskPrivateKey); if (dnsKeyTags is null) dnsKeyTags = zskPrivateKey.KeyTag.ToString(); else dnsKeyTags += ", " + zskPrivateKey.KeyTag.ToString(); } } if (zskToDeactivate is not null) DeactivateZskDnsKeys(zskToDeactivate); if (dnsKeyTags is not null) { _dnsServer.LogManager.Write("The ZSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were retired successfully: " + ToString()); return true; } return false; } private void DeactivateZskDnsKeys(IReadOnlyList zskPrivateKeys) { //remove all RRSIGs for the DNSKEYs List deletedRecords = new List(); IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); foreach (AuthZone zone in zones) { IReadOnlyList rrsigRecords = zone.GetRecords(DnsResourceRecordType.RRSIG); List rrsigsToRemove = new List(); foreach (DnsResourceRecord rrsigRecord in rrsigRecords) { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; foreach (DnssecPrivateKey privateKey in zskPrivateKeys) { if (rrsig.KeyTag == privateKey.KeyTag) { rrsigsToRemove.Add(rrsigRecord); break; } } } if (zone.TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList deletedRRSigRecords)) deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords); TriggerNotify(); string dnsKeyTags = null; foreach (DnssecPrivateKey privateKey in zskPrivateKeys) { if (dnsKeyTags is null) dnsKeyTags = privateKey.KeyTag.ToString(); else dnsKeyTags += ", " + privateKey.KeyTag.ToString(); } _dnsServer.LogManager.Write("The ZSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were deactivated successfully: " + ToString()); } private void RevokeKskDnsKeys(List kskPrivateKeys) { if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList existingDnsKeyRecords)) throw new InvalidOperationException(); List addedRecords = new List(); List deletedRecords = new List(); List dnsKeyRecords = new List(); foreach (DnsResourceRecord existingDnsKeyRecord in existingDnsKeyRecords) { bool found = false; foreach (DnssecPrivateKey privateKey in kskPrivateKeys) { if (existingDnsKeyRecord.RDATA.Equals(privateKey.DnsKey)) { found = true; break; } } if (!found) dnsKeyRecords.Add(existingDnsKeyRecord); } uint dnsKeyTtl = existingDnsKeyRecords[0].OriginalTtlValue; List keyTagsToRemove = new List(kskPrivateKeys.Count); //rfc7583#section-3.3.4 //modifiedQueryInterval = MAX(1hr, MIN(15 days, TTLkey / 2)) uint modifiedQueryInterval = Math.Max(3600u, Math.Min(15 * 24 * 60 * 60, dnsKeyTtl / 2)); foreach (DnssecPrivateKey privateKey in kskPrivateKeys) { keyTagsToRemove.Add(privateKey.KeyTag); privateKey.SetState(DnssecPrivateKeyState.Revoked, modifiedQueryInterval); DnsResourceRecord revokedDnsKeyRecord = new DnsResourceRecord(_name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, privateKey.DnsKey); dnsKeyRecords.Add(revokedDnsKeyRecord); } if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords)) throw new InvalidOperationException(); addedRecords.AddRange(dnsKeyRecords); deletedRecords.AddRange(deletedDnsKeyRecords); IReadOnlyList newRRSigRecords = SignRRSet(dnsKeyRecords); if (newRRSigRecords.Count > 0) { AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } //remove RRSIG for removed keys { IReadOnlyList rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG); List rrsigsToRemove = new List(); foreach (DnsResourceRecord rrsigRecord in rrsigRecords) { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; if (rrsig.TypeCovered != DnsResourceRecordType.DNSKEY) continue; foreach (ushort keyTagToRemove in keyTagsToRemove) { if (rrsig.KeyTag == keyTagToRemove) { rrsigsToRemove.Add(rrsigRecord); break; } } } if (TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList deletedRRSigRecords)) deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); //update revoked private keys string dnsKeyTags = null; lock (_dnssecPrivateKeys) { //remove old entry foreach (ushort keyTag in keyTagsToRemove) { if (_dnssecPrivateKeys.Remove(keyTag)) { if (dnsKeyTags is null) dnsKeyTags = keyTag.ToString(); else dnsKeyTags += ", " + keyTag.ToString(); } } //add new entry foreach (DnssecPrivateKey privateKey in kskPrivateKeys) _dnssecPrivateKeys.Add(privateKey.KeyTag, privateKey); } _dnsServer.LogManager.Write("The KSK DNSKEYs (" + dnsKeyTags + ") from the primary zone were revoked successfully: " + ToString()); } private void UnpublishDnsKeys(IReadOnlyList deadPrivateKeys) { if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList existingDnsKeyRecords)) throw new InvalidOperationException(); List addedRecords = new List(); List deletedRecords = new List(); List dnsKeyRecords = new List(); foreach (DnsResourceRecord existingDnsKeyRecord in existingDnsKeyRecords) { bool found = false; foreach (DnssecPrivateKey privateKey in deadPrivateKeys) { if (existingDnsKeyRecord.RDATA.Equals(privateKey.DnsKey)) { found = true; break; } } if (!found) dnsKeyRecords.Add(existingDnsKeyRecord); } if (dnsKeyRecords.Count < 2) throw new InvalidOperationException(); if (!TrySetRecords(DnsResourceRecordType.DNSKEY, dnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords)) throw new InvalidOperationException(); addedRecords.AddRange(dnsKeyRecords); deletedRecords.AddRange(deletedDnsKeyRecords); IReadOnlyList newRRSigRecords = SignRRSet(dnsKeyRecords); if (newRRSigRecords.Count > 0) { AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } //remove RRSig for revoked keys { IReadOnlyList rrsigRecords = GetRecords(DnsResourceRecordType.RRSIG); List rrsigsToRemove = new List(); foreach (DnsResourceRecord rrsigRecord in rrsigRecords) { DnsRRSIGRecordData rrsig = rrsigRecord.RDATA as DnsRRSIGRecordData; if (rrsig.TypeCovered != DnsResourceRecordType.DNSKEY) continue; foreach (DnssecPrivateKey privateKey in deadPrivateKeys) { if (rrsig.KeyTag == privateKey.KeyTag) { rrsigsToRemove.Add(rrsigRecord); break; } } } if (TryDeleteRecords(DnsResourceRecordType.RRSIG, rrsigsToRemove, out IReadOnlyList deletedRRSigRecords)) deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); //remove private keys permanently string dnsKeyTags = null; lock (_dnssecPrivateKeys) { foreach (DnssecPrivateKey privateKey in deadPrivateKeys) { if (_dnssecPrivateKeys.Remove(privateKey.KeyTag)) { if (dnsKeyTags is null) dnsKeyTags = privateKey.KeyTag.ToString(); else dnsKeyTags += ", " + privateKey.KeyTag.ToString(); } } } _dnsServer.LogManager.Write("The DNSKEYs (" + dnsKeyTags + ") from the primary zone were unpublished successfully: " + ToString()); } private async Task> GetDSPublishedPrivateKeysAsync(IReadOnlyList privateKeys, CancellationToken cancellationToken = default) { if (_name.Length == 0) return privateKeys; //zone is root //delete any existing DS entries from cache to allow resolving latest ones _dnsServer.CacheZoneManager.DeleteZone(_name); DirectDnsClient dnsClient = new DirectDnsClient(_dnsServer); dnsClient.DnssecValidation = true; dnsClient.Timeout = 10000; IReadOnlyList dsRecords; try { dsRecords = DnsClient.ParseResponseDS(await dnsClient.ResolveAsync(new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN), cancellationToken: cancellationToken)); } catch { //suppress exception here to avoid filling log file return []; } List activePrivateKeys = new List(dsRecords.Count); foreach (DnsDSRecordData dsRecord in dsRecords) { foreach (DnssecPrivateKey privateKey in privateKeys) { if ((dsRecord.KeyTag == privateKey.DnsKey.ComputedKeyTag) && (dsRecord.Algorithm == privateKey.DnsKey.Algorithm) && privateKey.DnsKey.IsDnsKeyValid(_name, dsRecord)) { activePrivateKeys.Add(privateKey); break; } } } return activePrivateKeys; } private bool TryRefreshAllSignatures() { List addedRecords = new List(); List deletedRecords = new List(); IReadOnlyList zones = _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name); foreach (AuthZone zone in zones) { IReadOnlyList newRRSigRecords = zone.RefreshSignatures(); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } } if ((deletedRecords.Count > 0) || (addedRecords.Count > 0)) { CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); return true; } return false; } internal override IReadOnlyList SignRRSet(IReadOnlyList records) { DnsResourceRecordType rrsetType = records[0].Type; List rrsigRecords = new List(); uint signatureValidityPeriod = GetSignatureValidityPeriod(); switch (rrsetType) { case DnsResourceRecordType.DNSKEY: lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if (privateKey.KeyType != DnssecPrivateKeyType.KeySigningKey) continue; switch (privateKey.State) { case DnssecPrivateKeyState.Published: case DnssecPrivateKeyState.Ready: case DnssecPrivateKeyState.Active: case DnssecPrivateKeyState.Revoked: rrsigRecords.Add(privateKey.SignRRSet(_name, records, DNSSEC_SIGNATURE_INCEPTION_OFFSET, signatureValidityPeriod)); break; } } } break; case DnsResourceRecordType.RRSIG: throw new InvalidOperationException(); case DnsResourceRecordType.ANAME: case DnsResourceRecordType.APP: throw new DnsServerException("Cannot sign RRSet: The record type [" + rrsetType.ToString() + "] is not supported by DNSSEC signed primary zones."); default: if ((rrsetType == DnsResourceRecordType.NS) && (records[0].Name.Length > _name.Length)) return Array.Empty(); //referrer NS records are not signed foreach (DnsResourceRecord record in records) { if (record.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot sign RRSet: Signing disabled records is not supported."); } lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { DnssecPrivateKey privateKey = privateKeyEntry.Value; if (privateKey.KeyType != DnssecPrivateKeyType.ZoneSigningKey) continue; switch (privateKey.State) { case DnssecPrivateKeyState.Ready: case DnssecPrivateKeyState.Active: rrsigRecords.Add(privateKey.SignRRSet(_name, records, DNSSEC_SIGNATURE_INCEPTION_OFFSET, signatureValidityPeriod)); break; } } } break; } if (rrsigRecords.Count == 0) throw new InvalidOperationException("Cannot sign RRSet: no private key was available."); return rrsigRecords; } internal void UpdateDnssecRecordsFor(AuthZone zone, DnsResourceRecordType type) { //lock to sync this call to prevent inconsistent NSEC/NSEC3 updates lock (_dnssecUpdateLock) { IReadOnlyList records = zone.GetRecords(type); if (records.Count > 0) { //rrset added or updated //sign rrset IReadOnlyList newRRSigRecords = SignRRSet(records); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); CommitAndIncrementSerial(deletedRRSigRecords, newRRSigRecords); } } else { //rrset deleted //delete rrsig IReadOnlyList existingRRSigRecords = zone.GetRecords(DnsResourceRecordType.RRSIG); if (existingRRSigRecords.Count > 0) { List recordsToDelete = new List(); foreach (DnsResourceRecord existingRRSigRecord in existingRRSigRecords) { DnsRRSIGRecordData rrsig = existingRRSigRecord.RDATA as DnsRRSIGRecordData; if (rrsig.TypeCovered == type) recordsToDelete.Add(existingRRSigRecord); } if (zone.TryDeleteRecords(DnsResourceRecordType.RRSIG, recordsToDelete, out IReadOnlyList deletedRRSigRecords)) CommitAndIncrementSerial(deletedRRSigRecords); } } if (_dnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC) { UpdateNSecRRSetFor(zone); } else { UpdateNSec3RRSetFor(zone); int apexLabelCount = DnsRRSIGRecordData.GetLabelCount(_name); int zoneLabelCount = DnsRRSIGRecordData.GetLabelCount(zone.Name); if (zone.Name.StartsWith("*.") || zone.Name.Equals('*')) zoneLabelCount++; //need to consider wildcard label for ENT detection if ((zoneLabelCount - apexLabelCount) > 1) { //empty non-terminal (ENT) may exists string currentOwnerName = zone.Name; while (true) { currentOwnerName = AuthZoneManager.GetParentZone(currentOwnerName); if (currentOwnerName.Equals(_name, StringComparison.OrdinalIgnoreCase)) break; //update NSEC3 rrset for current owner name AuthZone entZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, currentOwnerName); if (entZone is null) entZone = new PrimarySubDomainZone(null, currentOwnerName); //dummy empty non-terminal (ENT) sub domain object UpdateNSec3RRSetFor(entZone); } } } } } private void UpdateNSecRRSetFor(AuthZone zone) { uint ttl = GetZoneSoaMinimum(); IReadOnlyList newNSecRecords = GetUpdatedNSecRRSetFor(zone, ttl); if (newNSecRecords.Count > 0) { DnsResourceRecord newNSecRecord = newNSecRecords[0]; DnsNSECRecordData newNSec = newNSecRecord.RDATA as DnsNSECRecordData; if (newNSec.Types.Count == 2) { //only NSEC and RRSIG exists so remove NSEC IReadOnlyList deletedNSecRecords = zone.RemoveNSecRecordsWithRRSig(); if (deletedNSecRecords.Count > 0) CommitAndIncrementSerial(deletedNSecRecords); //relink previous nsec RelinkPreviousNSecRRSetFor(newNSecRecord, ttl, true); } else { List addedRecords = new List(); List deletedRecords = new List(); if (!zone.TrySetRecords(DnsResourceRecordType.NSEC, newNSecRecords, out IReadOnlyList deletedNSecRecords)) throw new DnsServerException("Failed to set DNSSEC records. Please try again."); addedRecords.AddRange(newNSecRecords); deletedRecords.AddRange(deletedNSecRecords); IReadOnlyList newRRSigRecords = SignRRSet(newNSecRecords); if (newRRSigRecords.Count > 0) { zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); if (deletedNSecRecords.Count == 0) { //new NSEC created since no old NSEC was removed //relink previous nsec RelinkPreviousNSecRRSetFor(newNSecRecord, ttl, false); } } } } private void UpdateNSec3RRSetFor(AuthZone zone) { uint ttl = GetZoneSoaMinimum(); bool noSubDomainExistsForEmptyZone = (zone.IsEmpty || zone.HasOnlyNSec3Records()) && !_dnsServer.AuthZoneManager.SubDomainExistsFor(_name, zone.Name); IReadOnlyList newNSec3Records = GetUpdatedNSec3RRSetFor(zone, ttl, noSubDomainExistsForEmptyZone); if (newNSec3Records.Count > 0) { DnsResourceRecord newNSec3Record = newNSec3Records[0]; AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetOrAddSubDomainZone(_name, newNSec3Record.Name); if (nsec3Zone is null) throw new InvalidOperationException(); if (noSubDomainExistsForEmptyZone) { //no records exists in real zone and no sub domain exists, so remove NSEC3 IReadOnlyList deletedNSec3Records = nsec3Zone.RemoveNSec3RecordsWithRRSig(); if (deletedNSec3Records.Count > 0) CommitAndIncrementSerial(deletedNSec3Records); //remove nsec3 sub domain zone if empty since it wont get removed otherwise if (nsec3Zone is SubDomainZone nsec3SubDomainZone) { if (nsec3Zone.IsEmpty) _dnsServer.AuthZoneManager.RemoveSubDomainZone(nsec3Zone.Name); //remove empty sub zone else nsec3SubDomainZone.AutoUpdateState(); } //remove the real zone if empty so that any of the ENT that exists can also be removed later if (zone is SubDomainZone subDomainZone) { if (zone.IsEmpty) _dnsServer.AuthZoneManager.RemoveSubDomainZone(zone.Name); //remove empty sub zone else subDomainZone.AutoUpdateState(); } //relink previous nsec3 RelinkPreviousNSec3RRSet(newNSec3Record, ttl, true); } else { List addedRecords = new List(); List deletedRecords = new List(); if (!nsec3Zone.TrySetRecords(DnsResourceRecordType.NSEC3, newNSec3Records, out IReadOnlyList deletedNSec3Records)) throw new DnsServerException("Failed to set DNSSEC records. Please try again."); addedRecords.AddRange(newNSec3Records); deletedRecords.AddRange(deletedNSec3Records); IReadOnlyList newRRSigRecords = SignRRSet(newNSec3Records); if (newRRSigRecords.Count > 0) { nsec3Zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); if (deletedNSec3Records.Count == 0) { //new NSEC3 created since no old NSEC3 was removed //relink previous nsec RelinkPreviousNSec3RRSet(newNSec3Record, ttl, false); } } } } private IReadOnlyList GetUpdatedNSecRRSetFor(AuthZone zone, uint ttl) { AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(_name, zone.Name); if (nextZone is null) nextZone = this; return zone.GetUpdatedNSecRRSet(nextZone.Name, ttl); } private IReadOnlyList GetUpdatedNSec3RRSetFor(AuthZone zone, uint ttl, bool forceGetNewRRSet) { if (!_entries.TryGetValue(DnsResourceRecordType.NSEC3PARAM, out IReadOnlyList nsec3ParamRecords)) throw new InvalidOperationException(); DnsResourceRecord nsec3ParamRecord = nsec3ParamRecords[0]; DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecord.RDATA as DnsNSEC3PARAMRecordData; string hashedOwnerName = nsec3Param.ComputeHashedOwnerNameBase32HexString(zone.Name) + (_name.Length > 0 ? "." + _name : ""); byte[] nextHashedOwnerName = null; //find next hashed owner name string currentOwnerName = hashedOwnerName; while (true) { AuthZone nextZone = _dnsServer.AuthZoneManager.FindNextSubDomainZone(_name, currentOwnerName); if (nextZone is null) break; IReadOnlyList nextNSec3Records = nextZone.GetRecords(DnsResourceRecordType.NSEC3); if (nextNSec3Records.Count > 0) { nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(nextNSec3Records[0].Name); break; } currentOwnerName = nextZone.Name; } if (nextHashedOwnerName is null) { //didnt find next NSEC3 record since current must be last; find the first NSEC3 record DnsResourceRecord previousNSec3Record = null; while (true) { AuthZone previousZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentOwnerName); if (previousZone is null) break; IReadOnlyList previousNSec3Records = previousZone.GetRecords(DnsResourceRecordType.NSEC3); if (previousNSec3Records.Count > 0) previousNSec3Record = previousNSec3Records[0]; currentOwnerName = previousZone.Name; } if (previousNSec3Record is not null) nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(previousNSec3Record.Name); } if (nextHashedOwnerName is null) nextHashedOwnerName = DnsNSEC3RecordData.GetHashedOwnerNameFrom(hashedOwnerName); //only 1 NSEC3 record in zone IReadOnlyList newNSec3Records = zone.CreateNSec3RRSet(hashedOwnerName, nextHashedOwnerName, ttl, nsec3Param.Iterations, nsec3Param.Salt); if (forceGetNewRRSet) return newNSec3Records; AuthZone nsec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, hashedOwnerName); if (nsec3Zone is null) return newNSec3Records; return nsec3Zone.GetUpdatedNSec3RRSet(newNSec3Records); } private void RelinkPreviousNSecRRSetFor(DnsResourceRecord currentNSecRecord, uint ttl, bool wasRemoved) { AuthZone previousNsecZone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentNSecRecord.Name); if (previousNsecZone is null) return; //current zone is apex IReadOnlyList newPreviousNSecRecords; if (wasRemoved) newPreviousNSecRecords = previousNsecZone.GetUpdatedNSecRRSet((currentNSecRecord.RDATA as DnsNSECRecordData).NextDomainName, ttl); else newPreviousNSecRecords = previousNsecZone.GetUpdatedNSecRRSet(currentNSecRecord.Name, ttl); if (newPreviousNSecRecords.Count > 0) { if (!previousNsecZone.TrySetRecords(DnsResourceRecordType.NSEC, newPreviousNSecRecords, out IReadOnlyList deletedNSecRecords)) throw new DnsServerException("Failed to set DNSSEC records. Please try again."); List addedRecords = new List(); List deletedRecords = new List(); addedRecords.AddRange(newPreviousNSecRecords); deletedRecords.AddRange(deletedNSecRecords); IReadOnlyList newRRSigRecords = SignRRSet(newPreviousNSecRecords); if (newRRSigRecords.Count > 0) { previousNsecZone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); } } private void RelinkPreviousNSec3RRSet(DnsResourceRecord currentNSec3Record, uint ttl, bool wasRemoved) { DnsNSEC3RecordData currentNSec3 = currentNSec3Record.RDATA as DnsNSEC3RecordData; //find the previous NSEC3 and update it DnsResourceRecord previousNSec3Record = null; AuthZone previousNSec3Zone; string currentOwnerName = currentNSec3Record.Name; while (true) { previousNSec3Zone = _dnsServer.AuthZoneManager.FindPreviousSubDomainZone(_name, currentOwnerName); if (previousNSec3Zone is null) break; IReadOnlyList previousNSec3Records = previousNSec3Zone.GetRecords(DnsResourceRecordType.NSEC3); if (previousNSec3Records.Count > 0) { previousNSec3Record = previousNSec3Records[0]; break; } currentOwnerName = previousNSec3Zone.Name; } if (previousNSec3Record is null) { //didnt find previous NSEC3; find the last NSEC3 to update if (wasRemoved) currentOwnerName = currentNSec3.NextHashedOwnerName + (_name.Length > 0 ? "." + _name : ""); else currentOwnerName = currentNSec3Record.Name; while (true) { AuthZone nextNSec3Zone = _dnsServer.AuthZoneManager.GetAuthZone(_name, currentOwnerName); if (nextNSec3Zone is null) break; IReadOnlyList nextNSec3Records = nextNSec3Zone.GetRecords(DnsResourceRecordType.NSEC3); if (nextNSec3Records.Count > 0) { previousNSec3Record = nextNSec3Records[0]; previousNSec3Zone = nextNSec3Zone; string nextHashedOwnerNameString = (previousNSec3Record.RDATA as DnsNSEC3RecordData).NextHashedOwnerName + (_name.Length > 0 ? "." + _name : ""); if (DnsNSECRecordData.CanonicalComparison(previousNSec3Record.Name, nextHashedOwnerNameString) >= 0) break; //found last NSEC3 //jump to next hashed owner currentOwnerName = nextHashedOwnerNameString; } else { currentOwnerName = nextNSec3Zone.Name; } } } if (previousNSec3Record is null) throw new InvalidOperationException(); DnsNSEC3RecordData previousNSec3 = previousNSec3Record.RDATA as DnsNSEC3RecordData; DnsNSEC3RecordData newPreviousNSec3; if (wasRemoved) newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, currentNSec3.NextHashedOwnerNameValue, previousNSec3.Types); else newPreviousNSec3 = new DnsNSEC3RecordData(DnssecNSEC3HashAlgorithm.SHA1, DnssecNSEC3Flags.None, previousNSec3.Iterations, previousNSec3.Salt, DnsNSEC3RecordData.GetHashedOwnerNameFrom(currentNSec3Record.Name), previousNSec3.Types); DnsResourceRecord[] newPreviousNSec3Records = new DnsResourceRecord[] { new DnsResourceRecord(previousNSec3Record.Name, DnsResourceRecordType.NSEC3, DnsClass.IN, ttl, newPreviousNSec3) }; if (!previousNSec3Zone.TrySetRecords(DnsResourceRecordType.NSEC3, newPreviousNSec3Records, out IReadOnlyList deletedNSec3Records)) throw new DnsServerException("Failed to set DNSSEC records. Please try again."); List addedRecords = new List(); List deletedRecords = new List(); addedRecords.AddRange(newPreviousNSec3Records); deletedRecords.AddRange(deletedNSec3Records); IReadOnlyList newRRSigRecords = SignRRSet(newPreviousNSec3Records); if (newRRSigRecords.Count > 0) { previousNSec3Zone.AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); } private uint GetSignatureValidityPeriod() { //SOA EXPIRE * 2 return GetZoneSoaExpire() * 2; } private uint GetPropagationDelay() { //the max time required to sync zone changes to secondaries if NOTIFY fails to trigger a zone transfer DnsSOARecordData soa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData; return soa.Refresh + soa.Retry; } private async Task GetParentSidePropagationDelayAsync(CancellationToken cancellationToken = default) { uint parentSidePropagationDelay = 24 * 60 * 60; try { string parent = AuthZoneManager.GetParentZone(_name); if (parent is null) parent = ""; DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(parent, DnsResourceRecordType.SOA, DnsClass.IN), 10000, cancellationToken: cancellationToken); if (soaResponse.RCODE == DnsResponseCode.NoError) { IReadOnlyList records; if (soaResponse.Answer.Count > 0) records = soaResponse.Answer; else if (soaResponse.Authority.Count > 0) records = soaResponse.Authority; else records = null; if (records is not null) { foreach (DnsResourceRecord record in records) { if (record.Type == DnsResourceRecordType.SOA) { DnsSOARecordData parentSoa = record.RDATA as DnsSOARecordData; parentSidePropagationDelay = parentSoa.Refresh + parentSoa.Retry; break; } } } } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } return parentSidePropagationDelay; } private uint GetMaxRRSigTtl() { uint maxTtl = 0; foreach (AuthZone zone in _dnsServer.AuthZoneManager.GetApexZoneWithSubDomainZones(_name)) { if (!zone.Entries.TryGetValue(DnsResourceRecordType.RRSIG, out IReadOnlyList rrsigRecords)) continue; foreach (DnsResourceRecord rr in rrsigRecords) { if (rr.OriginalTtlValue > maxTtl) maxTtl = rr.OriginalTtlValue; } } return maxTtl; } private async Task GetDSTtlAsync(CancellationToken cancellationToken = default) { uint dsTtl = 24 * 60 * 60; try { DnsDatagram dsResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(_name, DnsResourceRecordType.DS, DnsClass.IN), 10000, cancellationToken: cancellationToken); if (dsResponse.RCODE == DnsResponseCode.NoError) { if (dsResponse.Answer.Count > 0) { //find min TTL dsTtl = 0; foreach (DnsResourceRecord answer in dsResponse.Answer) { if (answer.Type == DnsResourceRecordType.DS) { if ((dsTtl == 0) || (dsTtl > answer.OriginalTtlValue)) dsTtl = answer.OriginalTtlValue; } } } else { dsTtl = 0; //no DS was found } } } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } return dsTtl; } public uint GetDnsKeyTtl() { if (_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList dnsKeyRecords)) return dnsKeyRecords[0].OriginalTtlValue; return 24 * 60 * 60; } public void UpdateDnsKeyTtl(uint dnsKeyTtl) { if (_dnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsServerException("The zone must be signed."); lock (_dnssecPrivateKeys) { foreach (KeyValuePair privateKeyEntry in _dnssecPrivateKeys) { switch (privateKeyEntry.Value.State) { case DnssecPrivateKeyState.Ready: case DnssecPrivateKeyState.Active: break; default: throw new DnsServerException("Cannot update DNSKEY TTL value: one or more private keys have state other than Ready or Active."); } } } if (!_entries.TryGetValue(DnsResourceRecordType.DNSKEY, out IReadOnlyList dnsKeyRecords)) throw new InvalidOperationException(); DnsResourceRecord[] newDnsKeyRecords = new DnsResourceRecord[dnsKeyRecords.Count]; for (int i = 0; i < dnsKeyRecords.Count; i++) { DnsResourceRecord dnsKeyRecord = dnsKeyRecords[i]; newDnsKeyRecords[i] = new DnsResourceRecord(dnsKeyRecord.Name, DnsResourceRecordType.DNSKEY, DnsClass.IN, dnsKeyTtl, dnsKeyRecord.RDATA); } List addedRecords = new List(); List deletedRecords = new List(); if (!TrySetRecords(DnsResourceRecordType.DNSKEY, newDnsKeyRecords, out IReadOnlyList deletedDnsKeyRecords)) throw new DnsServerException("Failed to update DNSKEY TTL. Please try again."); addedRecords.AddRange(newDnsKeyRecords); deletedRecords.AddRange(deletedDnsKeyRecords); IReadOnlyList newRRSigRecords = SignRRSet(newDnsKeyRecords); if (newRRSigRecords.Count > 0) { AddOrUpdateRRSigRecords(newRRSigRecords, out IReadOnlyList deletedRRSigRecords); addedRecords.AddRange(newRRSigRecords); deletedRecords.AddRange(deletedRRSigRecords); } CommitAndIncrementSerial(deletedRecords, addedRecords); TriggerNotify(); } #endregion #region versioning internal override void CommitAndIncrementSerial(IReadOnlyList deletedRecords = null, IReadOnlyList addedRecords = null) { if (_internal) { _lastModified = DateTime.UtcNow; return; } base.CommitAndIncrementSerial(deletedRecords, addedRecords); } #endregion #region public public override string GetZoneTypeName() { return "Primary"; } public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) { switch (type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.APP: throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones."); default: foreach (DnsResourceRecord record in records) { if (record.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot set records: disabling records in a signed zones is not supported."); } break; } } switch (type) { case DnsResourceRecordType.CNAME: case DnsResourceRecordType.DS: throw new InvalidOperationException("Cannot set " + type.ToString() + " record at zone apex."); case DnsResourceRecordType.SOA: if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Invalid SOA record."); DnsResourceRecord newSoaRecord = records[0]; DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData; if (newSoaRecord.OriginalTtlValue > newSoa.Expire) throw new DnsServerException("Cannot set record: TTL cannot be greater than SOA EXPIRE."); if (newSoa.Retry > newSoa.Refresh) throw new DnsServerException("Cannot set record: SOA RETRY cannot be greater than SOA REFRESH."); if (newSoa.Refresh > newSoa.Expire) throw new DnsServerException("Cannot set record: SOA REFRESH cannot be greater than SOA EXPIRE."); //remove any record info except serial date scheme and comments bool useSoaSerialDateScheme; string comments; { SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo(); useSoaSerialDateScheme = recordInfo.UseSoaSerialDateScheme; comments = recordInfo.Comments; } newSoaRecord.Tag = null; //remove old record info { SOARecordInfo recordInfo = newSoaRecord.GetAuthSOARecordInfo(); recordInfo.UseSoaSerialDateScheme = useSoaSerialDateScheme; recordInfo.Comments = comments; recordInfo.LastModified = DateTime.UtcNow; } uint oldSoaMinimum = GetZoneSoaMinimum(); //setting new SOA if (_internal) _entries[DnsResourceRecordType.SOA] = records; //update SOA directly else CommitAndIncrementSerial(null, records); if (oldSoaMinimum != newSoa.Minimum) { switch (_dnssecStatus) { case AuthZoneDnssecStatus.SignedWithNSEC: RefreshNSec(); break; case AuthZoneDnssecStatus.SignedWithNSEC3: RefreshNSec3(); break; } } TriggerNotify(); break; case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot set DNSSEC records."); case DnsResourceRecordType.FWD: throw new DnsServerException("The record type is not supported by primary zones."); default: if (records[0].OriginalTtlValue > GetZoneSoaExpire()) throw new DnsServerException("Cannot set records: TTL cannot be greater than SOA EXPIRE."); if (!TrySetRecords(type, records, out IReadOnlyList deletedRecords)) throw new DnsServerException("Cannot set records. Please try again."); CommitAndIncrementSerial(deletedRecords, records); if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) UpdateDnssecRecordsFor(this, type); TriggerNotify(); break; } } public override bool AddRecord(DnsResourceRecord record) { if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) { switch (record.Type) { case DnsResourceRecordType.ANAME: case DnsResourceRecordType.APP: throw new DnsServerException("The record type is not supported by DNSSEC signed primary zones."); default: if (record.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot add record: disabling records in a signed zones is not supported."); break; } } switch (record.Type) { case DnsResourceRecordType.APP: throw new InvalidOperationException("Cannot add record: use SetRecords() for " + record.Type.ToString() + " record"); case DnsResourceRecordType.DS: throw new InvalidOperationException("Cannot set DS record at zone apex."); case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot add DNSSEC record."); case DnsResourceRecordType.FWD: throw new DnsServerException("The record type is not supported by primary zones."); default: if (record.OriginalTtlValue > GetZoneSoaExpire()) throw new DnsServerException("Cannot add record: TTL cannot be greater than SOA EXPIRE."); AddRecord(record, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); if (addedRecords.Count > 0) { CommitAndIncrementSerial(deletedRecords, addedRecords); if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) UpdateDnssecRecordsFor(this, record.Type); TriggerNotify(); return true; } return false; } } public override bool DeleteRecords(DnsResourceRecordType type) { switch (type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot delete SOA record."); case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot delete DNSSEC records."); default: if (_entries.TryRemove(type, out IReadOnlyList removedRecords)) { CommitAndIncrementSerial(removedRecords); if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) UpdateDnssecRecordsFor(this, type); TriggerNotify(); return true; } return false; } } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData rdata) { switch (type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot delete SOA record."); case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot delete DNSSEC records."); default: if (TryDeleteRecord(type, rdata, out DnsResourceRecord deletedRecord)) { CommitAndIncrementSerial([deletedRecord]); if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) UpdateDnssecRecordsFor(this, type); TriggerNotify(); return true; } return false; } } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { switch (oldRecord.Type) { case DnsResourceRecordType.SOA: throw new InvalidOperationException("Cannot update record: use SetRecords() for " + oldRecord.Type.ToString() + " record"); case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3PARAM: case DnsResourceRecordType.NSEC3: throw new InvalidOperationException("Cannot update DNSSEC records."); default: if (oldRecord.Type != newRecord.Type) throw new InvalidOperationException("Old and new record types do not match."); if ((_dnssecStatus != AuthZoneDnssecStatus.Unsigned) && newRecord.GetAuthGenericRecordInfo().Disabled) throw new DnsServerException("Cannot update record: disabling records in a signed zones is not supported."); if (newRecord.OriginalTtlValue > GetZoneSoaExpire()) throw new DnsServerException("Cannot update record: TTL cannot be greater than SOA EXPIRE."); if (!TryDeleteRecord(oldRecord.Type, oldRecord.RDATA, out DnsResourceRecord deletedRecord)) throw new DnsServerException("Cannot update record: the record does not exists to be updated."); AddRecord(newRecord, out IReadOnlyList addedRecords, out IReadOnlyList deletedRecords); List allDeletedRecords = new List(deletedRecords.Count + 1); allDeletedRecords.Add(deletedRecord); allDeletedRecords.AddRange(deletedRecords); CommitAndIncrementSerial(allDeletedRecords, addedRecords); if (_dnssecStatus != AuthZoneDnssecStatus.Unsigned) UpdateDnssecRecordsFor(this, oldRecord.Type); TriggerNotify(); break; } } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { if (base.Disabled == value) return; base.Disabled = value; //set value early to be able to use it for notify if (value) DisableNotifyTimer(); else TriggerNotify(); } } public override AuthZoneTransfer ZoneTransfer { get { return base.ZoneTransfer; } set { if (_internal) throw new InvalidOperationException(); base.ZoneTransfer = value; } } public override AuthZoneNotify Notify { get { return base.Notify; } set { if (_internal) throw new InvalidOperationException(); switch (value) { case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones: throw new ArgumentException("The Notify option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Notify)); } base.Notify = value; } } public override AuthZoneUpdate Update { get { return base.Update; } set { if (_internal) throw new InvalidOperationException(); base.Update = value; } } public bool Internal { get { return _internal; } } public IReadOnlyCollection DnssecPrivateKeys { get { if (_dnssecPrivateKeys is null) return null; lock (_dnssecPrivateKeys) { return _dnssecPrivateKeys.Values; } } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/SecondaryCatalogSubDomainZone.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System.Collections.Generic; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class SecondaryCatalogSubDomainZone : SecondarySubDomainZone { #region constructor public SecondaryCatalogSubDomainZone(SecondaryZone secondaryZone, string name) : base(secondaryZone, name) { } #endregion #region public public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { return []; //secondary catalog zone is not queriable } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/SecondaryCatalogZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class SecondaryCatalogZone : SecondaryForwarderZone { #region events public event EventHandler ZoneAdded; public event EventHandler ZoneRemoved; #endregion #region variables readonly static IReadOnlyCollection _allowACL = [ new NetworkAccessControl(IPAddress.Any, 0), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; readonly static IReadOnlyCollection _queryAccessAllowOnlyPrivateNetworksACL = [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("10.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("100.64.0.0"), 10), new NetworkAccessControl(IPAddress.Parse("169.254.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("172.16.0.0"), 12), new NetworkAccessControl(IPAddress.Parse("192.168.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("2000::"), 3, true), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; readonly static IReadOnlyCollection _allowOnlyZoneNameServersACL = [ new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32) ]; readonly static IReadOnlyCollection _denyACL = [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("::1"), 128) ]; readonly static NetworkAccessControl _allowZoneNameServersAndUseSpecifiedNetworkACL = new NetworkAccessControl(IPAddress.Parse("224.0.0.0"), 32); Dictionary _membersIndex = new Dictionary(); #endregion #region constructor public SecondaryCatalogZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { } public SecondaryCatalogZone(DnsServer dnsServer, string name, IReadOnlyList primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) : base(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName) { } #endregion #region protected protected override void InitZone() { //init secondary catalog zone with dummy SOA and NS records DnsSOARecordData soa = new DnsSOARecordData("invalid", "invalid", 0, 300, 60, 604800, 900); DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _entries[DnsResourceRecordType.SOA] = [soaRecord]; _entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 0, new DnsNSRecordData("invalid"))]; } #endregion #region internal internal void BuildMembersIndex() { Dictionary membersIndex = new Dictionary(); foreach (KeyValuePair memberEntry in EnumerateCatalogMemberZones(_dnsServer)) membersIndex.TryAdd(memberEntry.Key.ToLowerInvariant(), memberEntry.Value); _membersIndex = membersIndex; } #endregion #region secondary catalog public IReadOnlyCollection GetAllMemberZoneNames() { return _membersIndex.Keys; } protected override async Task FinalizeZoneTransferAsync() { //secondary catalog does not maintain zone history; no need to call base method string version = GetVersion(); if ((version is null) || !version.Equals("2", StringComparison.OrdinalIgnoreCase)) { _dnsServer.LogManager.Write("Failed to provision Secondary Catalog zone '" + ToString() + "': catalog version not supported."); return; } Dictionary updatedMembersIndex = new Dictionary(); foreach (KeyValuePair memberEntry in EnumerateCatalogMemberZones(_dnsServer)) updatedMembersIndex.TryAdd(memberEntry.Key, memberEntry.Value); Dictionary membersToRemove = new Dictionary(); Dictionary membersToAdd = new Dictionary(); foreach (KeyValuePair memberEntry in _membersIndex) { if (!updatedMembersIndex.TryGetValue(memberEntry.Key, out string updatedMembersZoneDomain)) { //member was removed from catalog zone; remove local zone membersToRemove.Add(memberEntry.Key, null); } else if (!memberEntry.Value.Equals(updatedMembersZoneDomain, StringComparison.OrdinalIgnoreCase)) { //member exists but label does not match; reprovision zone membersToRemove.Add(memberEntry.Key, null); membersToAdd.Add(memberEntry.Key, updatedMembersZoneDomain); } } foreach (KeyValuePair updatedMemberEntry in updatedMembersIndex) { if (_membersIndex.TryGetValue(updatedMemberEntry.Key, out _)) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(updatedMemberEntry.Key); if (apexZone is not null) continue; //zone already exists; do nothing } //member was added to catalog zone; provision zone membersToAdd.TryAdd(updatedMemberEntry.Key, updatedMemberEntry.Value); } //set global custom properties UpdateGlobalAllowQueryProperty(); UpdateGlobalAllowTransferAndTsigKeyNamesProperties(); //add and remove member zones if ((membersToRemove.Count > 0) || (membersToAdd.Count > 0)) await AddAndRemoveMemberZonesAsync(membersToRemove, membersToAdd); //set member zone custom properties if (updatedMembersIndex.Count > 0) UpdateMemberZoneCustomProperties(updatedMembersIndex); _membersIndex = updatedMembersIndex; _dnsServer.AuthZoneManager.SaveZoneFile(_name); } protected override async Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList historyRecords) { //secondary catalog does not maintain zone history; no need to call base method string version = GetVersion(); if ((version is null) || !version.Equals("2", StringComparison.OrdinalIgnoreCase)) { _dnsServer.LogManager.Write("Failed to provision Secondary Catalog zone '" + ToString() + "': catalog version not supported."); return; } bool isAddHistoryRecord = false; bool updateGlobalAllowQueryProperty = false; bool updateGlobalAllowTransferAndTsigKeyNamesProperties = false; Dictionary membersToRemove = new Dictionary(); Dictionary membersToAdd = new Dictionary(); Dictionary membersToUpdate = new Dictionary(); //inspect records in history for (int i = 1; i < historyRecords.Count; i++) { DnsResourceRecord historyRecord = historyRecords[i]; if (historyRecord.Type == DnsResourceRecordType.SOA) { isAddHistoryRecord = true; //removed records completed continue; } if (historyRecord.Name.Length == _name.Length) continue; //skip apex records string subdomain = historyRecord.Name.Substring(0, historyRecord.Name.Length - _name.Length - 1).ToLowerInvariant(); string[] labels = subdomain.Split('.'); Array.Reverse(labels); switch (labels[0]) { case "ext": if (labels.Length > 1) { switch (labels[1]) { case "allow-query": updateGlobalAllowQueryProperty = true; break; case "allow-transfer": case "transfer-tsig-key-names": updateGlobalAllowTransferAndTsigKeyNamesProperties = true; break; } } break; case "zones": if (labels.Length == 2) { if (historyRecord.Type == DnsResourceRecordType.PTR) { string memberZoneName = (historyRecord.RDATA as DnsPTRRecordData).Domain.ToLowerInvariant(); if (isAddHistoryRecord) { string memberZoneDomain = subdomain + "." + _name; membersToAdd.TryAdd(memberZoneName, memberZoneDomain); membersToUpdate.TryAdd(memberZoneName, memberZoneDomain); } else { membersToRemove.TryAdd(memberZoneName, null); } } } else if (labels.Length > 2) { switch (labels[2]) { case "ext": case "coo": string memberZoneDomain = labels[1] + "." + labels[0] + "." + _name; DnsResourceRecord prevHistoryRecord = historyRecords[i - 1]; if (prevHistoryRecord.Name.EndsWith(memberZoneDomain, StringComparison.OrdinalIgnoreCase)) break; //skip since its same member zone's custom property IReadOnlyList ptrRecords = _dnsServer.AuthZoneManager.GetRecords(_name, memberZoneDomain, DnsResourceRecordType.PTR); if (ptrRecords.Count > 0) membersToUpdate.TryAdd((ptrRecords[0].RDATA as DnsPTRRecordData).Domain.ToLowerInvariant(), memberZoneDomain); break; } } break; } } //apply changes if (updateGlobalAllowQueryProperty) UpdateGlobalAllowQueryProperty(); if (updateGlobalAllowTransferAndTsigKeyNamesProperties) UpdateGlobalAllowTransferAndTsigKeyNamesProperties(); if ((membersToRemove.Count > 0) || (membersToAdd.Count > 0)) await AddAndRemoveMemberZonesAsync(membersToRemove, membersToAdd); if (membersToUpdate.Count > 0) UpdateMemberZoneCustomProperties(membersToUpdate); if ((membersToRemove.Count > 0) || (membersToAdd.Count > 0)) { //update members index Dictionary updatedMembersIndex = new Dictionary(_membersIndex); foreach (KeyValuePair removedMember in membersToRemove) updatedMembersIndex.Remove(removedMember.Key); foreach (KeyValuePair addedMember in membersToAdd) updatedMembersIndex.TryAdd(addedMember.Key, addedMember.Value); _membersIndex = updatedMembersIndex; } _dnsServer.AuthZoneManager.SaveZoneFile(_name); } private async Task AddAndRemoveMemberZonesAsync(Dictionary membersToRemove, Dictionary membersToAdd) { //remove zones foreach (KeyValuePair removeMember in membersToRemove) { ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(removeMember.Key); if ((apexZone is not null) && _name.Equals(apexZone.CatalogZoneName, StringComparison.OrdinalIgnoreCase)) DeleteMemberZone(apexZone); } //add zones List tasks = new List(membersToAdd.Count); foreach (KeyValuePair addMember in membersToAdd) { string zoneName = addMember.Key; string memberZoneDomain = addMember.Value; ApexZone apexZone = _dnsServer.AuthZoneManager.GetApexZone(zoneName); if (apexZone is null) { //create zone AuthZoneType zoneType = GetZoneTypeProperty(memberZoneDomain); switch (zoneType) { case AuthZoneType.Primary: { TaskCompletionSource taskSource = new TaskCompletionSource(); if (_dnsServer.TryQueueResolverTask(async delegate (object state) { //create secondary zone try { IReadOnlyList primaryNameServerAddresses; DnsTransportProtocol primaryZoneTransferProtocol; string primaryZoneTransferTsigKeyName; List> primaries = GetPrimariesProperty(memberZoneDomain); if (primaries.Count == 0) primaries = GetPrimariesProperty(_name); bool overrideCatalogPrimaryNameServers; if (primaries.Count > 0) { List primaryNameServerAddressesList = new List(); primaryNameServerAddresses = primaryNameServerAddressesList; primaryZoneTransferProtocol = DnsTransportProtocol.Tcp; primaryZoneTransferTsigKeyName = primaries[0].Item2; overrideCatalogPrimaryNameServers = true; foreach (Tuple primaryNameServer in primaries) { if (primaryNameServer.Item2 == primaryZoneTransferTsigKeyName) primaryNameServerAddressesList.Add(new NameServerAddress(primaryNameServer.Item1, DnsTransportProtocol.Tcp)); } } else { overrideCatalogPrimaryNameServers = false; primaryNameServerAddresses = PrimaryNameServerAddresses; primaryZoneTransferProtocol = PrimaryZoneTransferProtocol; primaryZoneTransferTsigKeyName = PrimaryZoneTransferTsigKeyName; } AuthZoneInfo zoneInfo = await _dnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, false, true); zoneInfo.OverrideCatalogPrimaryNameServers = overrideCatalogPrimaryNameServers; //set as catalog zone member zoneInfo.ApexZone.CatalogZoneName = _name; //raise event ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo)); //write log _dnsServer.LogManager.Write(zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' was added via Secondary Catalog zone '" + ToString() + "' sucessfully."); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { (state as TaskCompletionSource).TrySetResult(); } }, taskSource)) { tasks.Add(taskSource.Task); } } break; case AuthZoneType.Secondary: { TaskCompletionSource taskSource = new TaskCompletionSource(); if (_dnsServer.TryQueueResolverTask(async delegate (object state) { //create secondary zone try { IReadOnlyList primaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain); DnsTransportProtocol primaryZoneTransferProtocol = GetPrimaryZoneTransferProtocolProperty(memberZoneDomain); string primaryZoneTransferTsigKeyName = GetPrimaryZoneTransferTsigKeyNameProperty(memberZoneDomain); bool validateZone = GetZoneMdValidationProperty(memberZoneDomain); AuthZoneInfo zoneInfo = await _dnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone, true); zoneInfo.OverrideCatalogPrimaryNameServers = true; //always true for secondary member zones //set as catalog zone member zoneInfo.ApexZone.CatalogZoneName = _name; //raise event ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo)); //write log _dnsServer.LogManager.Write(zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' was added via Secondary Catalog zone '" + ToString() + "' sucessfully."); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { (state as TaskCompletionSource).TrySetResult(); } }, taskSource)) { tasks.Add(taskSource.Task); } } break; case AuthZoneType.Stub: { TaskCompletionSource taskSource = new TaskCompletionSource(); if (_dnsServer.TryQueueResolverTask(async delegate (object state) { //create stub zone try { IReadOnlyList primaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain); AuthZoneInfo zoneInfo = await _dnsServer.AuthZoneManager.CreateStubZoneAsync(zoneName, primaryNameServerAddresses, true); //set as catalog zone member zoneInfo.ApexZone.CatalogZoneName = _name; //raise event ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo)); //write log _dnsServer.LogManager.Write(zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' was added via Secondary Catalog zone '" + ToString() + "' sucessfully."); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } finally { (state as TaskCompletionSource).TrySetResult(); } }, taskSource)) { tasks.Add(taskSource.Task); } } break; case AuthZoneType.Forwarder: { //create secondary forwarder zone try { AuthZoneInfo zoneInfo = _dnsServer.AuthZoneManager.CreateSecondaryForwarderZone(zoneName, PrimaryNameServerAddresses, PrimaryZoneTransferProtocol, PrimaryZoneTransferTsigKeyName); //set as catalog zone member zoneInfo.ApexZone.CatalogZoneName = _name; //raise event ZoneAdded?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo)); //write log _dnsServer.LogManager.Write(zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' was added via Secondary Catalog zone '" + ToString() + "' sucessfully."); } catch (Exception ex) { _dnsServer.LogManager.Write(ex); } } break; } } } //wait for all zones to be added foreach (Task task in tasks) await task; } private void UpdateGlobalAllowQueryProperty() { //allow query global custom property IReadOnlyCollection globalAllowQueryACL = GetAllowQueryProperty(_name); if (globalAllowQueryACL.Count > 0) { _queryAccess = GetQueryAccessType(globalAllowQueryACL); switch (_queryAccess) { case AuthZoneQueryAccess.UseSpecifiedNetworkACL: QueryAccessNetworkACL = globalAllowQueryACL; break; case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: QueryAccessNetworkACL = GetFilteredACL(globalAllowQueryACL); break; default: QueryAccessNetworkACL = null; break; } } else { _queryAccess = AuthZoneQueryAccess.Allow; QueryAccessNetworkACL = null; } } private void UpdateGlobalAllowTransferAndTsigKeyNamesProperties() { //allow transfer global custom property IReadOnlyCollection globalAllowTransferACL = GetAllowTransferProperty(_name); if (globalAllowTransferACL.Count > 0) { _zoneTransfer = GetZoneTransferType(globalAllowTransferACL); switch (_zoneTransfer) { case AuthZoneTransfer.UseSpecifiedNetworkACL: ZoneTransferNetworkACL = globalAllowTransferACL; break; case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: ZoneTransferNetworkACL = GetFilteredACL(globalAllowTransferACL); break; default: ZoneTransferNetworkACL = null; break; } //zone tranfer tsig key names global custom property ZoneTransferTsigKeyNames = GetZoneTransferTsigKeyNamesProperty(_name); } else { _zoneTransfer = AuthZoneTransfer.Deny; ZoneTransferNetworkACL = null; ZoneTransferTsigKeyNames = null; } } private void UpdateMemberZoneCustomProperties(Dictionary membersToUpdate) { foreach (KeyValuePair updatedMemberEntry in membersToUpdate) { string zoneName = updatedMemberEntry.Key; string memberZoneDomain = updatedMemberEntry.Value; ApexZone memberApexZone = _dnsServer.AuthZoneManager.GetApexZone(zoneName); if ((memberApexZone is not null) && _name.Equals(memberApexZone.CatalogZoneName, StringComparison.OrdinalIgnoreCase)) { //change of ownership property { string newCatalogZoneName = GetChangeOfOwnershipProperty(memberZoneDomain); if (newCatalogZoneName is not null) { ApexZone catalogApexZone = _dnsServer.AuthZoneManager.GetApexZone(newCatalogZoneName); if (catalogApexZone is SecondaryCatalogZone secondaryCatalogZone) { //found secondary catalog zone; transfer ownership to it memberApexZone.CatalogZoneName = secondaryCatalogZone._name; } else { //no such secondary catalog zone exists; delete member zone DeleteMemberZone(memberApexZone); continue; } } } //allow query member zone custom property { IReadOnlyCollection allowQueryACL = GetAllowQueryProperty(memberZoneDomain); if (allowQueryACL.Count > 0) { memberApexZone.QueryAccess = GetQueryAccessType(allowQueryACL); switch (memberApexZone.QueryAccess) { case AuthZoneQueryAccess.UseSpecifiedNetworkACL: memberApexZone.QueryAccessNetworkACL = allowQueryACL; break; case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: memberApexZone.QueryAccessNetworkACL = GetFilteredACL(allowQueryACL); break; default: memberApexZone.QueryAccessNetworkACL = null; break; } memberApexZone.OverrideCatalogQueryAccess = true; } else { memberApexZone.OverrideCatalogQueryAccess = false; memberApexZone.QueryAccess = AuthZoneQueryAccess.Allow; memberApexZone.QueryAccessNetworkACL = null; } } if (memberApexZone is StubZone stubZone) { //primary addresses property stubZone.PrimaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain); } else if (memberApexZone is SecondaryForwarderZone) { //do nothing } else if (memberApexZone is SecondaryZone secondaryZone) { AuthZoneType zoneType = GetZoneTypeProperty(memberZoneDomain); if (zoneType == AuthZoneType.Secondary) { secondaryZone.PrimaryNameServerAddresses = GetPrimaryAddressesProperty(memberZoneDomain); secondaryZone.PrimaryZoneTransferProtocol = GetPrimaryZoneTransferProtocolProperty(memberZoneDomain); secondaryZone.PrimaryZoneTransferTsigKeyName = GetPrimaryZoneTransferTsigKeyNameProperty(memberZoneDomain); secondaryZone.ValidateZone = GetZoneMdValidationProperty(memberZoneDomain); } else { //primaries property List> primaries = GetPrimariesProperty(memberZoneDomain); if (primaries.Count == 0) primaries = GetPrimariesProperty(_name); if (primaries.Count > 0) { List primaryNameServerAddresses = new List(); string primaryZoneTransferTsigKeyName = primaries[0].Item2; foreach (Tuple primaryNameServer in primaries) { if (primaryNameServer.Item2 == primaryZoneTransferTsigKeyName) primaryNameServerAddresses.Add(new NameServerAddress(primaryNameServer.Item1, DnsTransportProtocol.Tcp)); } secondaryZone.PrimaryNameServerAddresses = primaryNameServerAddresses; secondaryZone.PrimaryZoneTransferProtocol = DnsTransportProtocol.Tcp; secondaryZone.PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName; secondaryZone.OverrideCatalogPrimaryNameServers = true; } else { secondaryZone.OverrideCatalogPrimaryNameServers = false; secondaryZone.PrimaryNameServerAddresses = null; secondaryZone.PrimaryZoneTransferProtocol = DnsTransportProtocol.Tcp; secondaryZone.PrimaryZoneTransferTsigKeyName = null; } } //allow transfer member zone custom property IReadOnlyCollection allowTransferACL = GetAllowTransferProperty(memberZoneDomain); if (allowTransferACL.Count > 0) { memberApexZone.ZoneTransfer = GetZoneTransferType(allowTransferACL); switch (memberApexZone.ZoneTransfer) { case AuthZoneTransfer.UseSpecifiedNetworkACL: memberApexZone.ZoneTransferNetworkACL = allowTransferACL; break; case AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL: memberApexZone.ZoneTransferNetworkACL = GetFilteredACL(allowTransferACL); break; default: memberApexZone.ZoneTransferNetworkACL = null; break; } //zone tranfer tsig key names member zone custom property memberApexZone.ZoneTransferTsigKeyNames = GetZoneTransferTsigKeyNamesProperty(memberZoneDomain); memberApexZone.OverrideCatalogZoneTransfer = true; } else { memberApexZone.OverrideCatalogZoneTransfer = false; memberApexZone.ZoneTransfer = AuthZoneTransfer.Deny; memberApexZone.ZoneTransferNetworkACL = null; memberApexZone.ZoneTransferTsigKeyNames = null; } } _dnsServer.AuthZoneManager.SaveZoneFile(memberApexZone.Name); } } } private void DeleteMemberZone(ApexZone apexZone) { AuthZoneInfo zoneInfo = new AuthZoneInfo(apexZone); if (_dnsServer.AuthZoneManager.DeleteZone(zoneInfo, true)) { ZoneRemoved?.Invoke(this, new SecondaryCatalogEventArgs(zoneInfo)); _dnsServer.LogManager.Write(apexZone.GetZoneTypeName() + " zone '" + apexZone.ToString() + "' was removed via Secondary Catalog zone '" + ToString() + "' sucessfully."); } } private string GetVersion() { string domain = "version." + _name; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT); if (records.Count > 0) return (records[0].RDATA as DnsTXTRecordData).GetText(); return null; } private string GetChangeOfOwnershipProperty(string memberZoneDomain) { string domain = "coo." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.PTR); if (records.Count > 0) return (records[0].RDATA as DnsPTRRecordData).Domain; return null; } private AuthZoneType GetZoneTypeProperty(string memberZoneDomain) { string domain = "zone-type.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT); if (records.Count > 0) return Enum.Parse((records[0].RDATA as DnsTXTRecordData).GetText(), true); return AuthZoneType.Primary; } private List> GetPrimariesProperty(string memberZoneDomain) { string domain = "primaries.ext." + memberZoneDomain; List> primaries = new List>(2); AuthZone authZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, domain); if (authZone is not null) { foreach (DnsResourceRecord record in authZone.GetRecords(DnsResourceRecordType.A)) primaries.Add(new Tuple((record.RDATA as DnsARecordData).Address, null)); foreach (DnsResourceRecord record in authZone.GetRecords(DnsResourceRecordType.AAAA)) primaries.Add(new Tuple((record.RDATA as DnsAAAARecordData).Address, null)); } List subdomains = new List(); _dnsServer.AuthZoneManager.ListSubDomains(domain, subdomains); foreach (string subdomain in subdomains) { AuthZone subZone = _dnsServer.AuthZoneManager.GetAuthZone(_name, subdomain + "." + domain); if (subZone is null) continue; string tsigKeyName = null; IReadOnlyList szTXTRecords = subZone.GetRecords(DnsResourceRecordType.TXT); if (szTXTRecords.Count > 0) tsigKeyName = (szTXTRecords[0].RDATA as DnsTXTRecordData).GetText(); foreach (DnsResourceRecord record in subZone.GetRecords(DnsResourceRecordType.A)) primaries.Add(new Tuple((record.RDATA as DnsARecordData).Address, tsigKeyName)); foreach (DnsResourceRecord record in subZone.GetRecords(DnsResourceRecordType.AAAA)) primaries.Add(new Tuple((record.RDATA as DnsAAAARecordData).Address, tsigKeyName)); } return primaries; } private IReadOnlyList GetPrimaryAddressesProperty(string memberZoneDomain) { string domain = "primary-addresses.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT); if (records.Count > 0) return (records[0].RDATA as DnsTXTRecordData).CharacterStrings.Convert(NameServerAddress.Parse); return []; } private DnsTransportProtocol GetPrimaryZoneTransferProtocolProperty(string memberZoneDomain) { string domain = "primary-transfer-protocol.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT); if (records.Count > 0) return Enum.Parse((records[0].RDATA as DnsTXTRecordData).CharacterStrings[0], true); return DnsTransportProtocol.Tcp; } private string GetPrimaryZoneTransferTsigKeyNameProperty(string memberZoneDomain) { string domain = "primary-transfer-tsig-key-name.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.PTR); if (records.Count > 0) return (records[0].RDATA as DnsPTRRecordData).Domain; return null; } private bool GetZoneMdValidationProperty(string memberZoneDomain) { string domain = "zonemd-validation.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.TXT); if (records.Count > 0) return bool.Parse((records[0].RDATA as DnsTXTRecordData).CharacterStrings[0]); return false; } private IReadOnlyCollection GetAllowQueryProperty(string memberZoneDomain) { string domain = "allow-query.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.APL); if (records.Count > 0) return NetworkAccessControl.ConvertFromAPLRecordData(records[0].RDATA as DnsAPLRecordData); return []; } private IReadOnlyCollection GetAllowTransferProperty(string memberZoneDomain) { string domain = "allow-transfer.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.APL); if (records.Count > 0) return NetworkAccessControl.ConvertFromAPLRecordData(records[0].RDATA as DnsAPLRecordData); return []; } private HashSet GetZoneTransferTsigKeyNamesProperty(string memberZoneDomain) { string domain = "transfer-tsig-key-names.ext." + memberZoneDomain; IReadOnlyList records = _dnsServer.AuthZoneManager.GetRecords(_name, domain, DnsResourceRecordType.PTR); HashSet keyNames = new HashSet(records.Count); foreach (DnsResourceRecord record in records) keyNames.Add((record.RDATA as DnsPTRRecordData).Domain.ToLowerInvariant()); return keyNames; } private static AuthZoneQueryAccess GetQueryAccessType(IReadOnlyCollection acl) { if (acl.HasSameItems(_allowACL)) return AuthZoneQueryAccess.Allow; if (acl.HasSameItems(_queryAccessAllowOnlyPrivateNetworksACL)) return AuthZoneQueryAccess.AllowOnlyPrivateNetworks; if (acl.HasSameItems(_allowOnlyZoneNameServersACL)) return AuthZoneQueryAccess.AllowOnlyZoneNameServers; if ((acl.Count > 1) && acl.Contains(_allowZoneNameServersAndUseSpecifiedNetworkACL)) return AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL; if (acl.HasSameItems(_denyACL)) return AuthZoneQueryAccess.Deny; return AuthZoneQueryAccess.UseSpecifiedNetworkACL; } private static AuthZoneTransfer GetZoneTransferType(IReadOnlyCollection acl) { if (acl.HasSameItems(_allowACL)) return AuthZoneTransfer.Allow; if (acl.HasSameItems(_allowOnlyZoneNameServersACL)) return AuthZoneTransfer.AllowOnlyZoneNameServers; if ((acl.Count > 1) && acl.Contains(_allowZoneNameServersAndUseSpecifiedNetworkACL)) return AuthZoneTransfer.AllowZoneNameServersAndUseSpecifiedNetworkACL; if (acl.HasSameItems(_denyACL)) return AuthZoneTransfer.Deny; return AuthZoneTransfer.UseSpecifiedNetworkACL; } private static List GetFilteredACL(IReadOnlyCollection acl) { List filteredACL = new List(acl.Count); foreach (NetworkAccessControl ac in acl) { if (ac.Equals(_allowZoneNameServersAndUseSpecifiedNetworkACL)) continue; filteredACL.Add(ac); } return filteredACL; } #endregion #region public public override string GetZoneTypeName() { return "Secondary Catalog"; } public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { return []; //secondary catalog zone is not queriable } #endregion #region properties public override string CatalogZoneName { get { return base.CatalogZoneName; } set { throw new InvalidOperationException(); } } public override bool OverrideCatalogQueryAccess { get { throw new InvalidOperationException(); } set { throw new InvalidOperationException(); } } public override AuthZoneQueryAccess QueryAccess { get { return _queryAccess; } set { throw new InvalidOperationException(); } } public override AuthZoneTransfer ZoneTransfer { get { return _zoneTransfer; } set { throw new InvalidOperationException(); } } public override AuthZoneUpdate Update { get { return base.Update; } set { throw new InvalidOperationException(); } } #endregion } public class SecondaryCatalogEventArgs : EventArgs { #region variables readonly AuthZoneInfo _zoneInfo; #endregion #region constructor public SecondaryCatalogEventArgs(AuthZoneInfo zoneInfo) { _zoneInfo = zoneInfo; } #endregion #region properties public AuthZoneInfo ZoneInfo { get { return _zoneInfo; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/SecondaryForwarderZone.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class SecondaryForwarderZone : SecondaryZone { #region constructor public SecondaryForwarderZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { } public SecondaryForwarderZone(DnsServer dnsServer, string name, IReadOnlyList primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null) : base(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, false) { InitZone(); } #endregion #region protected protected virtual void InitZone() { //init secondary forwarder zone with dummy SOA record DnsSOARecordData soa = new DnsSOARecordData(_dnsServer.ServerDomain, "invalid", 0, 900, 300, 604800, 900); DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; _entries[DnsResourceRecordType.SOA] = [soaRecord]; } protected override Task FinalizeZoneTransferAsync() { //secondary forwarder does not maintain zone history; no need to call base method return Task.CompletedTask; } protected override Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList historyRecords) { //secondary forwarder does not maintain zone history; no need to call base method return Task.CompletedTask; } #endregion #region public public override string GetZoneTypeName() { return "Secondary Forwarder"; } public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { if (type == DnsResourceRecordType.SOA) return []; //secondary forwarder zone is not authoritative and contains dummy SOA record return base.QueryRecords(type, dnssecOk); } #endregion #region properties public override bool OverrideCatalogZoneTransfer { get { throw new InvalidOperationException(); } set { throw new InvalidOperationException(); } } public override bool OverrideCatalogPrimaryNameServers { get { throw new InvalidOperationException(); } set { throw new InvalidOperationException(); } } public override AuthZoneQueryAccess QueryAccess { get { return base.QueryAccess; } set { switch (value) { case AuthZoneQueryAccess.AllowOnlyZoneNameServers: case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Query Access option is invalid for Secondary Conditional Forwarder zones: " + value.ToString(), nameof(QueryAccess)); } base.QueryAccess = value; } } public override AuthZoneTransfer ZoneTransfer { get { return base.ZoneTransfer; } set { throw new InvalidOperationException(); } } public override AuthZoneNotify Notify { get { return base.Notify; } set { throw new InvalidOperationException(); } } public override AuthZoneUpdate Update { get { return base.Update; } set { switch (value) { case AuthZoneUpdate.AllowOnlyZoneNameServers: case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Dynamic Updates option is invalid for Secondary Conditional Forwarder zones: " + value.ToString(), nameof(Update)); } base.Update = value; } } public override IReadOnlyList PrimaryNameServerAddresses { get { return base.PrimaryNameServerAddresses; } set { if ((value is null) || (value.Count == 0)) throw new ArgumentException("At least one primary name server address must be specified for " + GetZoneTypeName() + " zone.", nameof(PrimaryNameServerAddresses)); base.PrimaryNameServerAddresses = value; } } public override bool ValidateZone { get { return base.ValidateZone; } set { throw new InvalidOperationException(); } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/SecondarySubDomainZone.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.Collections.Generic; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class SecondarySubDomainZone : SubDomainZone { #region variables readonly SecondaryZone _secondaryZone; #endregion #region constructor public SecondarySubDomainZone(SecondaryZone secondaryZone, string name) : base(secondaryZone, name) { _secondaryZone = secondaryZone; } #endregion #region public public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { throw new InvalidOperationException("Cannot set records in " + _secondaryZone.GetZoneTypeName() + " zone."); } public override bool AddRecord(DnsResourceRecord record) { throw new InvalidOperationException("Cannot add record in " + _secondaryZone.GetZoneTypeName() + " zone."); } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) { throw new InvalidOperationException("Cannot delete record in " + _secondaryZone.GetZoneTypeName() + " zone."); } public override bool DeleteRecords(DnsResourceRecordType type) { throw new InvalidOperationException("Cannot delete records in " + _secondaryZone.GetZoneTypeName() + " zone."); } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { throw new InvalidOperationException("Cannot update record in " + _secondaryZone.GetZoneTypeName() + " zone."); } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/SecondaryZone.cs ================================================ /* 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.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { //Message Digest for DNS Zones //https://datatracker.ietf.org/doc/rfc8976/ class SecondaryZone : ApexZone { #region variables IReadOnlyCollection _dnssecPrivateKeys; //for holding DNSSEC private keys as a backup on secondary cluster nodes readonly object _refreshTimerLock = new object(); Timer _refreshTimer; bool _refreshTimerTriggered; const int REFRESH_TIMER_INTERVAL = 5000; const int REFRESH_SOA_TIMEOUT = 10000; const int REFRESH_XFR_TIMEOUT = 120000; const int REFRESH_RETRIES = 5; const int REFRESH_TSIG_FUDGE = 300; bool _overrideCatalogPrimaryNameServers; IReadOnlyList _primaryNameServerAddresses; DnsTransportProtocol _primaryZoneTransferProtocol; string _primaryZoneTransferTsigKeyName; DateTime _expiry; bool _isExpired; bool _validateZone; bool _validationFailed; bool _resync; #endregion #region constructor public SecondaryZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { _dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys; _overrideCatalogPrimaryNameServers = zoneInfo.OverrideCatalogPrimaryNameServers; _primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses; _primaryZoneTransferProtocol = zoneInfo.PrimaryZoneTransferProtocol; _primaryZoneTransferTsigKeyName = zoneInfo.PrimaryZoneTransferTsigKeyName; _expiry = zoneInfo.Expiry; _isExpired = DateTime.UtcNow > _expiry; _validateZone = zoneInfo.ValidateZone; _validationFailed = zoneInfo.ValidationFailed; _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); InitNotify(); } protected SecondaryZone(DnsServer dnsServer, string name, IReadOnlyList primaryNameServerAddresses, DnsTransportProtocol primaryZoneTransferProtocol, string primaryZoneTransferTsigKeyName, bool validateZone) : base(dnsServer, name) { PrimaryZoneTransferProtocol = primaryZoneTransferProtocol; PrimaryNameServerAddresses = primaryNameServerAddresses?.Convert(delegate (NameServerAddress nameServer) { if (nameServer.Protocol != primaryZoneTransferProtocol) nameServer = nameServer.Clone(primaryZoneTransferProtocol); return nameServer; }); PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName; _validateZone = validateZone; _isExpired = true; //new secondary zone is considered expired till it refreshes _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); InitNotify(); } #endregion #region static public static async Task CreateAsync(DnsServer dnsServer, string name, IReadOnlyList primaryNameServerAddresses = null, DnsTransportProtocol primaryZoneTransferProtocol = DnsTransportProtocol.Tcp, string primaryZoneTransferTsigKeyName = null, bool validateZone = false, bool ignoreSoaFailure = false) { SecondaryZone secondaryZone = new SecondaryZone(dnsServer, name, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone); try { DnsDatagram soaResponse; DnsQuestionRecord soaQuestion = new DnsQuestionRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN); if (secondaryZone.PrimaryNameServerAddresses is null) { soaResponse = await secondaryZone._dnsServer.DirectQueryAsync(soaQuestion); } else { DnsClient dnsClient = new DnsClient(secondaryZone.PrimaryNameServerAddresses); List tasks = new List(dnsClient.Servers.Count); foreach (NameServerAddress nameServerAddress in dnsClient.Servers) { if (nameServerAddress.IsIPEndPointStale) tasks.Add(nameServerAddress.ResolveIPAddressAsync(secondaryZone._dnsServer, secondaryZone._dnsServer.PreferIPv6)); } await Task.WhenAll(tasks); dnsClient.Proxy = secondaryZone._dnsServer.Proxy; dnsClient.PreferIPv6 = secondaryZone._dnsServer.PreferIPv6; DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [soaQuestion], null, null, null, secondaryZone._dnsServer.UdpPayloadSize); if (string.IsNullOrEmpty(primaryZoneTransferTsigKeyName)) soaResponse = await dnsClient.RawResolveAsync(soaRequest); else if ((secondaryZone._dnsServer.TsigKeys is not null) && secondaryZone._dnsServer.TsigKeys.TryGetValue(primaryZoneTransferTsigKeyName, out TsigKey key)) soaResponse = await dnsClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE); else throw new DnsServerException("No such TSIG key was found configured: " + primaryZoneTransferTsigKeyName); } if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA)) throw new DnsServerException("DNS Server did not receive SOA record in response from any of the primary name servers for: " + secondaryZone.ToString()); DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0]; DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData; DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum); DnsResourceRecord soaRecord = new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, receivedSoaRecord.OriginalTtlValue, soa); secondaryZone._entries[DnsResourceRecordType.SOA] = [soaRecord]; } catch { if (!ignoreSoaFailure) throw; //continue with dummy SOA DnsSOARecordData soa = new DnsSOARecordData(secondaryZone._dnsServer.ServerDomain, "invalid", 0, 300, 60, 604800, 900); DnsResourceRecord soaRecord = new DnsResourceRecord(secondaryZone._name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; secondaryZone._entries[DnsResourceRecordType.SOA] = [soaRecord]; } return secondaryZone; } #endregion #region IDisposable bool _disposed; protected override void Dispose(bool disposing) { try { if (_disposed) return; if (disposing) { lock (_refreshTimerLock) { if (_refreshTimer != null) { _refreshTimer.Dispose(); _refreshTimer = null; } } } _disposed = true; } finally { base.Dispose(disposing); } } #endregion #region private private void RefreshTimerCallback(object state) { //refresh zone in DNS server's resolver thread pool if (!_dnsServer.TryQueueResolverTask(async delegate (object state) { try { if (Disabled && !_resync) return; _isExpired = DateTime.UtcNow > _expiry; //get primary name server addresses IReadOnlyList primaryNameServerAddresses; DnsTransportProtocol primaryZoneTransferProtocol; string primaryZoneTransferTsigKeyName; SecondaryCatalogZone secondaryCatalogZone = SecondaryCatalogZone; if ((secondaryCatalogZone is not null) && !_overrideCatalogPrimaryNameServers) { primaryNameServerAddresses = await GetResolvedNameServerAddressesAsync(secondaryCatalogZone.PrimaryNameServerAddresses); primaryZoneTransferProtocol = secondaryCatalogZone.PrimaryZoneTransferProtocol; primaryZoneTransferTsigKeyName = secondaryCatalogZone.PrimaryZoneTransferTsigKeyName; } else { primaryNameServerAddresses = await GetResolvedPrimaryNameServerAddressesAsync(); primaryZoneTransferProtocol = _primaryZoneTransferProtocol; primaryZoneTransferTsigKeyName = _primaryZoneTransferTsigKeyName; } DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData; if (primaryNameServerAddresses.Count == 0) { _dnsServer.LogManager.Write("DNS Server could not find primary name server IP addresses for " + GetZoneTypeName() + " zone: " + ToString()); //set timer for retry ResetRefreshTimer(Math.Max(currentSoa.Retry, _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; return; } TsigKey key = null; if (!string.IsNullOrEmpty(primaryZoneTransferTsigKeyName) && ((_dnsServer.TsigKeys is null) || !_dnsServer.TsigKeys.TryGetValue(primaryZoneTransferTsigKeyName, out key))) { _dnsServer.LogManager.Write("DNS Server does not have TSIG key '" + primaryZoneTransferTsigKeyName + "' configured for refreshing " + GetZoneTypeName() + " zone: " + ToString()); //set timer for retry ResetRefreshTimer(Math.Max(currentSoa.Retry, _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; return; } //refresh zone if (await RefreshZoneAsync(primaryNameServerAddresses, primaryZoneTransferProtocol, key, _validateZone)) { DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData; _syncFailed = false; _expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire); _isExpired = false; _resync = false; _dnsServer.AuthZoneManager.SaveZoneFile(_name); if (_validationFailed) ResetRefreshTimer(Math.Max(latestSoa.Retry, _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); //zone validation failed, set timer for retry else ResetRefreshTimer(Math.Max(latestSoa.Refresh, _dnsServer.AuthZoneManager.MinSoaRefresh) * 1000); //zone refreshed; set timer for refresh return; } //no response from any of the name servers; set timer for retry ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; } catch (Exception ex) { _dnsServer.LogManager.Write(ex); //set timer for retry ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; } finally { _refreshTimerTriggered = false; } }) ) { //failed to queue refresh zone task; try again in some time lock (_refreshTimerLock) { _refreshTimer?.Change(REFRESH_TIMER_INTERVAL, Timeout.Infinite); } } } private void ResetRefreshTimer(long dueTime) { lock (_refreshTimerLock) { _refreshTimer?.Change(dueTime, Timeout.Infinite); } } private async Task RefreshZoneAsync(IReadOnlyList primaryNameServers, DnsTransportProtocol zoneTransferProtocol, TsigKey key, bool validateZone) { try { _dnsServer.LogManager.Write("DNS Server has started zone refresh for " + GetZoneTypeName() + " zone: " + ToString()); //get nameservers list with correct zone tranfer protocol List updatedNameServers = new List(primaryNameServers.Count); { switch (zoneTransferProtocol) { case DnsTransportProtocol.Tls: case DnsTransportProtocol.Quic: //change name server protocol to TLS/QUIC foreach (NameServerAddress primaryNameServer in primaryNameServers) { if (primaryNameServer.Protocol == zoneTransferProtocol) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.Clone(zoneTransferProtocol)); } break; default: //change name server protocol to TCP foreach (NameServerAddress primaryNameServer in primaryNameServers) { if (primaryNameServer.Protocol == DnsTransportProtocol.Tcp) updatedNameServers.Add(primaryNameServer); else updatedNameServers.Add(primaryNameServer.Clone(DnsTransportProtocol.Tcp)); } break; } } //init XFR DNS Client DnsClient xfrClient = new DnsClient(updatedNameServers); xfrClient.Proxy = _dnsServer.Proxy; xfrClient.PreferIPv6 = _dnsServer.PreferIPv6; xfrClient.Retries = REFRESH_RETRIES; xfrClient.Concurrency = 1; DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData; if (!_resync && (this is not SecondaryForwarderZone)) //skip SOA probe for Secondary Forwarder/Catalog since Forwarder/Catalog is not authoritative for SOA { //check for update xfrClient.Timeout = REFRESH_SOA_TIMEOUT; DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN)], null, null, null, _dnsServer.UdpPayloadSize); DnsDatagram soaResponse; if (key is null) soaResponse = await xfrClient.RawResolveAsync(soaRequest); else soaResponse = await xfrClient.TsigResolveAsync(soaRequest, key, REFRESH_TSIG_FUDGE); if (soaResponse.RCODE != DnsResponseCode.NoError) { _dnsServer.LogManager.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + ToString() + "' " + GetZoneTypeName() + " zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase)) { _dnsServer.LogManager.Write("DNS Server received an empty response for SOA query for '" + ToString() + "' " + GetZoneTypeName() + " zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0]; DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData; //compare using sequence space arithmetic if (!currentSoa.IsZoneUpdateAvailable(receivedSoa)) { _dnsServer.LogManager.Write("DNS Server successfully checked for '" + ToString() + "' " + GetZoneTypeName() + " zone update from: " + soaResponse.Metadata.NameServer.ToString()); return true; } } //update available; do zone transfer xfrClient.Timeout = REFRESH_XFR_TIMEOUT; bool doIXFR = !_isExpired && !_resync; while (true) { DnsQuestionRecord xfrQuestion; IReadOnlyList xfrAuthority; if (doIXFR) { xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.IXFR, DnsClass.IN); xfrAuthority = [currentSoaRecord]; } else { xfrQuestion = new DnsQuestionRecord(_name, DnsResourceRecordType.AXFR, DnsClass.IN); xfrAuthority = null; } DnsDatagram xfrRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [xfrQuestion], null, xfrAuthority); DnsDatagram xfrResponse; if (key is null) xfrResponse = await xfrClient.RawResolveAsync(xfrRequest); else xfrResponse = await xfrClient.TsigResolveAsync(xfrRequest, key, REFRESH_TSIG_FUDGE); if (doIXFR && ((xfrResponse.RCODE == DnsResponseCode.NotImplemented) || (xfrResponse.RCODE == DnsResponseCode.Refused))) { doIXFR = false; continue; } if (xfrResponse.RCODE != DnsResponseCode.NoError) { _dnsServer.LogManager.Write("DNS Server received a zone transfer response (RCODE=" + xfrResponse.RCODE.ToString() + ") for '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString()); return false; } if (xfrResponse.Answer.Count < 1) { _dnsServer.LogManager.Write("DNS Server received an empty response for zone transfer query for '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString()); return false; } if (!_name.Equals(xfrResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase) || (xfrResponse.Answer[0].RDATA is not DnsSOARecordData xfrSoa)) { _dnsServer.LogManager.Write("DNS Server received invalid response for zone transfer query for '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString()); return false; } if (_resync || currentSoa.IsZoneUpdateAvailable(xfrSoa)) { xfrResponse = xfrResponse.Join(); //join multi message response if (doIXFR) { IReadOnlyList historyRecords = _dnsServer.AuthZoneManager.SyncIncrementalZoneTransferRecords(_name, xfrResponse.Answer); if (historyRecords.Count > 0) await FinalizeIncrementalZoneTransferAsync(historyRecords); else await FinalizeZoneTransferAsync(); //AXFR response was received } else { _dnsServer.AuthZoneManager.SyncZoneTransferRecords(_name, xfrResponse.Answer); await FinalizeZoneTransferAsync(); } _lastModified = DateTime.UtcNow; if (validateZone) await ValidateZoneAsync(); else _validationFailed = false; if (_validationFailed) { _dnsServer.LogManager.Write("DNS Server refreshed '" + ToString() + "' " + GetZoneTypeName() + " zone with validation failure from: " + xfrResponse.Metadata.NameServer.ToString()); } else { //trigger notify TriggerNotify(); _dnsServer.LogManager.Write("DNS Server successfully refreshed '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + xfrResponse.Metadata.NameServer.ToString()); } } else { _dnsServer.LogManager.Write("DNS Server successfully checked for '" + ToString() + "' " + GetZoneTypeName() + " zone update from: " + xfrResponse.Metadata.NameServer.ToString()); } return true; } } catch (Exception ex) { _dnsServer.LogManager.Write("DNS Server failed to refresh '" + ToString() + "' " + GetZoneTypeName() + " zone from: " + primaryNameServers.Join() + "\r\n" + ex.ToString()); return false; } } private async Task ValidateZoneAsync(CancellationToken cancellationToken = default) { try { DirectDnsClient dnsClient = new DirectDnsClient(_dnsServer); dnsClient.DnssecValidation = true; dnsClient.Timeout = 10000; DnsDatagram zoneMdResponse = await dnsClient.ResolveAsync(_name, DnsResourceRecordType.ZONEMD, cancellationToken); IReadOnlyList zoneMdList = DnsClient.ParseResponseZONEMD(zoneMdResponse); if (zoneMdList.Count == 0) { //ZONEMD RRSet does not exists; digest verification cannot occur _validationFailed = false; _dnsServer.LogManager.Write("ZONEMD validation cannot occur for the " + GetZoneTypeName() + " zone '" + ToString() + "': ZONEMD RRset does not exists in the zone."); return; } for (int i = 0; i < zoneMdList.Count; i++) { for (int j = 0; j < zoneMdList.Count; j++) { if (i == j) continue; //skip comparing self DnsZONEMDRecordData zoneMd = zoneMdList[i]; DnsZONEMDRecordData checkZoneMd = zoneMdList[j]; if ((checkZoneMd.Scheme == zoneMd.Scheme) && (checkZoneMd.HashAlgorithm == zoneMd.HashAlgorithm)) { _validationFailed = true; _dnsServer.LogManager.Write("ZONEMD validation failed for the " + GetZoneTypeName() + " zone '" + ToString() + "': ZONEMD RRset contains more than one RR with the same Scheme and Hash Algorithm."); return; } } } DnsDatagram soaResponse = await dnsClient.ResolveAsync(_name, DnsResourceRecordType.SOA, cancellationToken); DnsSOARecordData soa = DnsClient.ParseResponseSOA(soaResponse); if (soa is null) { _validationFailed = true; _dnsServer.LogManager.Write("ZONEMD validation failed for the " + GetZoneTypeName() + " zone '" + ToString() + "': failed to find SOA record."); return; } using MemoryStream hashStream = new MemoryStream(4096); byte[] computedDigestSHA384 = null; byte[] computedDigestSHA512 = null; bool zoneSerialized = false; foreach (DnsZONEMDRecordData zoneMd in zoneMdList) { if (soa.Serial != zoneMd.Serial) continue; if (zoneMd.Scheme != ZoneMdScheme.Simple) continue; byte[] computedDigest; switch (zoneMd.HashAlgorithm) { case ZoneMdHashAlgorithm.SHA384: if (zoneMd.Digest.Length != 48) continue; if (computedDigestSHA384 is null) { if (!zoneSerialized) { SerializeZoneTo(hashStream); zoneSerialized = true; } hashStream.Position = 0; computedDigestSHA384 = SHA384.HashData(hashStream); } computedDigest = computedDigestSHA384; break; case ZoneMdHashAlgorithm.SHA512: if (zoneMd.Digest.Length != 64) continue; if (computedDigestSHA512 is null) { if (!zoneSerialized) { SerializeZoneTo(hashStream); zoneSerialized = true; } hashStream.Position = 0; computedDigestSHA512 = SHA512.HashData(hashStream); } computedDigest = computedDigestSHA512; break; default: continue; } if (computedDigest.ListEquals(zoneMd.Digest)) { //validation successfull _validationFailed = false; _dnsServer.LogManager.Write("ZONEMD validation was completed successfully for the " + GetZoneTypeName() + " zone: " + ToString()); return; } } //validation failed _validationFailed = true; _dnsServer.LogManager.Write("ZONEMD validation failed for the " + GetZoneTypeName() + " zone '" + ToString() + "': none of the ZONEMD records could successfully validate the zone."); } catch (Exception ex) { //validation failed _validationFailed = true; _dnsServer.LogManager.Write("ZONEMD validation failed for the " + GetZoneTypeName() + " zone '" + ToString() + "':\r\n" + ex.ToString()); } } private void SerializeZoneTo(MemoryStream hashStream) { //list zone records for ZONEMD Simple scheme List records; { List allZoneRecords = new List(); _dnsServer.AuthZoneManager.ListAllZoneRecords(_name, allZoneRecords); records = new List(allZoneRecords.Count); foreach (DnsResourceRecord record in allZoneRecords) { switch (record.Type) { case DnsResourceRecordType.NS: records.Add(record); IReadOnlyList glueRecords = record.GetAuthNSRecordInfo().GlueRecords; if (glueRecords is not null) records.AddRange(glueRecords); break; case DnsResourceRecordType.RRSIG: if (record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase) && (record.RDATA is DnsRRSIGRecordData rdata) && (rdata.TypeCovered == DnsResourceRecordType.ZONEMD)) break; //skip RRSIG covering the apex ZONEMD records.Add(record); break; case DnsResourceRecordType.ZONEMD: if (record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) break; //skip apex ZONEMD records.Add(record); break; default: records.Add(record); break; } } } //group records into zones by DNS name List>>> zones = new List>>>(DnsResourceRecord.GroupRecords(records, true)); //sort zones by canonical DNS name zones.Sort(delegate (KeyValuePair>> x, KeyValuePair>> y) { return DnsNSECRecordData.CanonicalComparison(x.Key, y.Key); }); //start serialization, zone by zone using MemoryStream rrBuffer = new MemoryStream(512); foreach (KeyValuePair>> zone in zones) { //list all RRSets for current zone owner name List>> rrSets = new List>>(zone.Value); //RRsets having the same owner name MUST be numerically ordered, in ascending order, by their numeric RR TYPE rrSets.Sort(delegate (KeyValuePair> x, KeyValuePair> y) { return x.Key.CompareTo(y.Key); }); //serialize records List rrList = new List(rrSets.Count * 4); foreach (KeyValuePair> rrSet in rrSets) { //serialize current RRSet records List serializedResourceRecords = new List(rrSet.Value.Count); foreach (DnsResourceRecord record in rrSet.Value) serializedResourceRecords.Add(CanonicallySerializedResourceRecord.Create(record.Name, record.Type, record.Class, record.OriginalTtlValue, record.RDATA, rrBuffer)); //Canonical RR Ordering by sorting RDATA portion of the canonical form of each RR serializedResourceRecords.Sort(); foreach (CanonicallySerializedResourceRecord serializedResourceRecord in serializedResourceRecords) serializedResourceRecord.WriteTo(hashStream); } } } protected virtual Task FinalizeZoneTransferAsync() { ClearZoneHistory(); return Task.CompletedTask; } protected virtual Task FinalizeIncrementalZoneTransferAsync(IReadOnlyList historyRecords) { CommitZoneHistory(historyRecords); return Task.CompletedTask; } #endregion #region public public override string GetZoneTypeName() { return "Secondary"; } public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL) { if (Disabled) return; if (_refreshTimerTriggered) return; _refreshTimerTriggered = true; ResetRefreshTimer(refreshInterval); } public void TriggerResync() { if (_refreshTimerTriggered) return; _resync = true; _refreshTimerTriggered = true; ResetRefreshTimer(0); } public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { throw new InvalidOperationException("Cannot set records in " + GetZoneTypeName() + " zone."); } public override bool AddRecord(DnsResourceRecord record) { throw new InvalidOperationException("Cannot add record in " + GetZoneTypeName() + " zone."); } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) { throw new InvalidOperationException("Cannot delete record in " + GetZoneTypeName() + " zone."); } public override bool DeleteRecords(DnsResourceRecordType type) { throw new InvalidOperationException("Cannot delete records in " + GetZoneTypeName() + " zone."); } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { throw new InvalidOperationException("Cannot update record in " + GetZoneTypeName() + " zone."); } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { if (base.Disabled == value) return; base.Disabled = value; //set value early to be able to use it for notify if (value) { DisableNotifyTimer(); ResetRefreshTimer(Timeout.Infinite); } else { TriggerNotify(); TriggerRefresh(); } } } public override bool OverrideCatalogNotify { get { //return true so that notification does not trigger when secondary zone is a member of catalog return true; } set { throw new InvalidOperationException(); } } public virtual bool OverrideCatalogPrimaryNameServers { get { return _overrideCatalogPrimaryNameServers; } set { _overrideCatalogPrimaryNameServers = value; } } public override AuthZoneNotify Notify { get { return base.Notify; } set { switch (value) { case AuthZoneNotify.SeparateNameServersForCatalogAndMemberZones: throw new ArgumentException("The Notify option is invalid for " + GetZoneTypeName() + " zones: " + value.ToString(), nameof(Notify)); } base.Notify = value; } } public override AuthZoneUpdate Update { get { return base.Update; } set { switch (value) { case AuthZoneUpdate.AllowOnlyZoneNameServers: case AuthZoneUpdate.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Dynamic Updates option is invalid for Secondary zones: " + value.ToString(), nameof(Update)); } base.Update = value; } } public virtual IReadOnlyList PrimaryNameServerAddresses { get { return _primaryNameServerAddresses; } set { if ((value is null) || (value.Count == 0)) _primaryNameServerAddresses = null; else if (value.Count > byte.MaxValue) throw new ArgumentOutOfRangeException(nameof(PrimaryNameServerAddresses), "Name server addresses cannot have more than 255 entries."); else _primaryNameServerAddresses = value; //update catalog zone property if (!Disabled) CatalogZone?.SetPrimaryAddressesProperty(_primaryNameServerAddresses, _name); } } public DnsTransportProtocol PrimaryZoneTransferProtocol { get { return _primaryZoneTransferProtocol; } set { switch (value) { case DnsTransportProtocol.Tcp: case DnsTransportProtocol.Tls: case DnsTransportProtocol.Quic: _primaryZoneTransferProtocol = value; //update catalog zone property if (!Disabled) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_primaryZoneTransferProtocol != DnsTransportProtocol.Tcp) catalogZone.SetPrimaryZoneTransferProtocolProperty(_primaryZoneTransferProtocol, _name); //update member zone custom property else catalogZone.SetPrimaryZoneTransferProtocolProperty(null, _name); //remove member zone custom property } } break; default: throw new NotSupportedException("Zone transfer protocol is not supported: XFR-over-" + value.ToString().ToUpper()); } } } public string PrimaryZoneTransferTsigKeyName { get { return _primaryZoneTransferTsigKeyName; } set { if (value is null) _primaryZoneTransferTsigKeyName = string.Empty; else _primaryZoneTransferTsigKeyName = value; //update catalog zone property if (!Disabled) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_primaryZoneTransferTsigKeyName.Length > 0) catalogZone.SetPrimaryZoneTransferTsigKeyNameProperty(_primaryZoneTransferTsigKeyName, _name); //update member zone custom property else catalogZone.SetPrimaryZoneTransferTsigKeyNameProperty(null, _name); //remove member zone custom property } } } } public DateTime Expiry { get { return _expiry; } } public bool IsExpired { get { return _isExpired; } } public virtual bool ValidateZone { get { return _validateZone; } set { _validateZone = value; //update catalog zone property if (!Disabled) { CatalogZone catalogZone = CatalogZone; if (catalogZone is not null) { if (_validateZone) catalogZone.SetZoneMdValidationProperty(_validateZone, _name); //update member zone custom property else catalogZone.SetZoneMdValidationProperty(null, _name); //remove member zone custom property } } } } public bool ValidationFailed { get { return _validationFailed; } } public override bool IsActive { get { return !Disabled && !_isExpired && !_validationFailed; } } public IReadOnlyCollection DnssecPrivateKeys { get { return _dnssecPrivateKeys; } set { _dnssecPrivateKeys = value; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/StubZone.cs ================================================ /* 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.Dns.ResourceRecords; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { class StubZone : ApexZone { #region variables readonly object _refreshTimerLock = new object(); Timer _refreshTimer; bool _refreshTimerTriggered; const int REFRESH_TIMER_INTERVAL = 5000; const int REFRESH_TIMEOUT = 10000; const int REFRESH_RETRIES = 5; IReadOnlyList _primaryNameServerAddresses; DateTime _expiry; bool _isExpired; bool _resync; #endregion #region constructor public StubZone(DnsServer dnsServer, AuthZoneInfo zoneInfo) : base(dnsServer, zoneInfo) { _primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses; _expiry = zoneInfo.Expiry; _isExpired = DateTime.UtcNow > _expiry; _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } private StubZone(DnsServer dnsServer, string name, IReadOnlyList primaryNameServerAddresses) : base(dnsServer, name) { PrimaryNameServerAddresses = primaryNameServerAddresses?.Convert(delegate (NameServerAddress nameServer) { if (nameServer.Protocol != DnsTransportProtocol.Udp) nameServer = nameServer.Clone(DnsTransportProtocol.Udp); return nameServer; }); _isExpired = true; //new stub zone is considered expired till it refreshes _refreshTimer = new Timer(RefreshTimerCallback, null, Timeout.Infinite, Timeout.Infinite); } #endregion #region static public static async Task CreateAsync(DnsServer dnsServer, string name, IReadOnlyList primaryNameServerAddresses = null, bool ignoreSoaFailure = false) { StubZone stubZone = new StubZone(dnsServer, name, primaryNameServerAddresses); try { DnsDatagram soaResponse; DnsQuestionRecord soaQuestion = new DnsQuestionRecord(name, DnsResourceRecordType.SOA, DnsClass.IN); if (stubZone.PrimaryNameServerAddresses is null) { soaResponse = await stubZone._dnsServer.DirectQueryAsync(soaQuestion); } else { DnsClient dnsClient = new DnsClient(stubZone.PrimaryNameServerAddresses); List tasks = new List(dnsClient.Servers.Count); foreach (NameServerAddress nameServerAddress in dnsClient.Servers) { if (nameServerAddress.IsIPEndPointStale) tasks.Add(nameServerAddress.ResolveIPAddressAsync(stubZone._dnsServer, stubZone._dnsServer.PreferIPv6)); } await Task.WhenAll(tasks); dnsClient.Proxy = stubZone._dnsServer.Proxy; dnsClient.PreferIPv6 = stubZone._dnsServer.PreferIPv6; DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [soaQuestion], null, null, null, dnsServer.UdpPayloadSize); soaResponse = await dnsClient.RawResolveAsync(soaRequest); } if ((soaResponse.Answer.Count == 0) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA)) throw new DnsServerException("DNS Server did not receive SOA record in response from any of the primary name servers for: " + name); DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0]; DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData; DnsSOARecordData soa = new DnsSOARecordData(receivedSoa.PrimaryNameServer, receivedSoa.ResponsiblePerson, 0u, receivedSoa.Refresh, receivedSoa.Retry, receivedSoa.Expire, receivedSoa.Minimum); DnsResourceRecord soaRecord = new DnsResourceRecord(stubZone._name, DnsResourceRecordType.SOA, DnsClass.IN, receivedSoaRecord.TTL, soa); stubZone._entries[DnsResourceRecordType.SOA] = [soaRecord]; } catch { if (!ignoreSoaFailure) throw; //continue with dummy SOA DnsSOARecordData soa = new DnsSOARecordData(stubZone._dnsServer.ServerDomain, "invalid", 0, 300, 60, 604800, 900); DnsResourceRecord soaRecord = new DnsResourceRecord(stubZone._name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa); soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; stubZone._entries[DnsResourceRecordType.SOA] = [soaRecord]; } return stubZone; } #endregion #region IDisposable bool _disposed; protected override void Dispose(bool disposing) { try { if (_disposed) return; if (disposing) { lock (_refreshTimerLock) { if (_refreshTimer != null) { _refreshTimer.Dispose(); _refreshTimer = null; } } } _disposed = true; } finally { base.Dispose(disposing); } } #endregion #region private private void RefreshTimerCallback(object state) { //refresh zone in DNS server's resolver thread pool if (!_dnsServer.TryQueueResolverTask(async delegate (object state) { try { if (Disabled && !_resync) return; _isExpired = DateTime.UtcNow > _expiry; //get primary name server addresses IReadOnlyList primaryNameServers = await GetResolvedPrimaryNameServerAddressesAsync(); if (primaryNameServers.Count == 0) { _dnsServer.LogManager.Write("DNS Server could not find primary name server IP addresses for Stub zone: " + ToString()); //set timer for retry ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; return; } //refresh zone if (await RefreshZoneAsync(primaryNameServers)) { //zone refreshed; set timer for refresh DnsSOARecordData latestSoa = _entries[DnsResourceRecordType.SOA][0].RDATA as DnsSOARecordData; ResetRefreshTimer(Math.Max(latestSoa.Refresh, _dnsServer.AuthZoneManager.MinSoaRefresh) * 1000); _syncFailed = false; _expiry = DateTime.UtcNow.AddSeconds(latestSoa.Expire); _isExpired = false; _resync = false; _dnsServer.AuthZoneManager.SaveZoneFile(_name); return; } //no response from any of the name servers; set timer for retry ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; } catch (Exception ex) { _dnsServer.LogManager.Write(ex); //set timer for retry ResetRefreshTimer(Math.Max(GetZoneSoaRetry(), _dnsServer.AuthZoneManager.MinSoaRetry) * 1000); _syncFailed = true; } finally { _refreshTimerTriggered = false; } }) ) { //failed to queue refresh zone task; try again in some time lock (_refreshTimerLock) { _refreshTimer?.Change(REFRESH_TIMER_INTERVAL, Timeout.Infinite); } } } private void ResetRefreshTimer(long dueTime) { lock (_refreshTimerLock) { _refreshTimer?.Change(dueTime, Timeout.Infinite); } } private async Task RefreshZoneAsync(IReadOnlyList nameServers) { try { _dnsServer.LogManager.Write("DNS Server has started zone refresh for Stub zone: " + ToString()); DnsClient client = new DnsClient(nameServers); client.Proxy = _dnsServer.Proxy; client.PreferIPv6 = _dnsServer.PreferIPv6; client.Timeout = REFRESH_TIMEOUT; client.Retries = REFRESH_RETRIES; client.Concurrency = 1; DnsDatagram soaRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, [new DnsQuestionRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN)], null, null, null, _dnsServer.UdpPayloadSize); DnsDatagram soaResponse = await client.RawResolveAsync(soaRequest); if (soaResponse.RCODE != DnsResponseCode.NoError) { _dnsServer.LogManager.Write("DNS Server received RCODE=" + soaResponse.RCODE.ToString() + " for '" + ToString() + "' Stub zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } if ((soaResponse.Answer.Count < 1) || (soaResponse.Answer[0].Type != DnsResourceRecordType.SOA) || !_name.Equals(soaResponse.Answer[0].Name, StringComparison.OrdinalIgnoreCase)) { _dnsServer.LogManager.Write("DNS Server received an empty response for SOA query for '" + ToString() + "' Stub zone refresh from: " + soaResponse.Metadata.NameServer.ToString()); return false; } DnsResourceRecord currentSoaRecord = _entries[DnsResourceRecordType.SOA][0]; DnsResourceRecord receivedSoaRecord = soaResponse.Answer[0]; DnsSOARecordData currentSoa = currentSoaRecord.RDATA as DnsSOARecordData; DnsSOARecordData receivedSoa = receivedSoaRecord.RDATA as DnsSOARecordData; //compare using sequence space arithmetic if (!_resync && !currentSoa.IsZoneUpdateAvailable(receivedSoa)) { _dnsServer.LogManager.Write("DNS Server successfully checked for '" + ToString() + "' Stub zone update from: " + soaResponse.Metadata.NameServer.ToString()); return true; } //update available; do zone sync with TCP transport List tcpNameServers = new List(); foreach (NameServerAddress nameServer in nameServers) tcpNameServers.Add(nameServer.Clone(DnsTransportProtocol.Tcp)); client = new DnsClient(tcpNameServers); client.Proxy = _dnsServer.Proxy; client.PreferIPv6 = _dnsServer.PreferIPv6; client.Timeout = REFRESH_TIMEOUT; client.Retries = REFRESH_RETRIES; client.Concurrency = 1; DnsDatagram nsRequest = new DnsDatagram(0, false, DnsOpcode.StandardQuery, false, false, false, false, false, false, DnsResponseCode.NoError, new DnsQuestionRecord[] { new DnsQuestionRecord(_name, DnsResourceRecordType.NS, DnsClass.IN) }); DnsDatagram nsResponse = await client.RawResolveAsync(nsRequest); if (nsResponse.RCODE != DnsResponseCode.NoError) { _dnsServer.LogManager.Write("DNS Server received RCODE=" + nsResponse.RCODE.ToString() + " for '" + ToString() + "' Stub zone refresh from: " + nsResponse.Metadata.NameServer.ToString()); return false; } if (nsResponse.Answer.Count < 1) { _dnsServer.LogManager.Write("DNS Server received an empty response for NS query for '" + ToString() + "' Stub zone from: " + nsResponse.Metadata.NameServer.ToString()); return false; } //prepare sync records List nsRecords = new List(nsResponse.Answer.Count); foreach (DnsResourceRecord record in nsResponse.Answer) { if ((record.Type == DnsResourceRecordType.NS) && record.Name.Equals(_name, StringComparison.OrdinalIgnoreCase)) { record.SyncGlueRecords(nsResponse.Additional); nsRecords.Add(record); } } receivedSoaRecord.CopyRecordInfoFrom(currentSoaRecord); //sync records _entries[DnsResourceRecordType.NS] = nsRecords; _entries[DnsResourceRecordType.SOA] = [receivedSoaRecord]; _lastModified = DateTime.UtcNow; _dnsServer.LogManager.Write("DNS Server successfully refreshed '" + ToString() + "' Stub zone from: " + nsResponse.Metadata.NameServer.ToString()); return true; } catch (Exception ex) { string strNameServers = null; foreach (NameServerAddress nameServer in nameServers) { if (strNameServers == null) strNameServers = nameServer.ToString(); else strNameServers += ", " + nameServer.ToString(); } _dnsServer.LogManager.Write("DNS Server failed to refresh '" + ToString() + "' Stub zone from: " + strNameServers + "\r\n" + ex.ToString()); return false; } } #endregion #region public public override string GetZoneTypeName() { return "Stub"; } public void TriggerRefresh(int refreshInterval = REFRESH_TIMER_INTERVAL) { if (Disabled) return; if (_refreshTimerTriggered) return; _refreshTimerTriggered = true; ResetRefreshTimer(refreshInterval); } public void TriggerResync() { if (_refreshTimerTriggered) return; _resync = true; _refreshTimerTriggered = true; ResetRefreshTimer(0); } public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records) { throw new InvalidOperationException("Cannot set records in Stub zone."); } public override bool AddRecord(DnsResourceRecord record) { throw new InvalidOperationException("Cannot add record in Stub zone."); } public override bool DeleteRecords(DnsResourceRecordType type) { throw new InvalidOperationException("Cannot delete record in Stub zone."); } public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record) { throw new InvalidOperationException("Cannot delete records in Stub zone."); } public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) { throw new InvalidOperationException("Cannot update record in Stub zone."); } public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk) { return []; //stub zone has no authority so cant return any records as query response to allow generating referral response } #endregion #region properties public override bool Disabled { get { return base.Disabled; } set { if (base.Disabled == value) return; base.Disabled = value; //set value early to be able to use it for refresh if (value) ResetRefreshTimer(Timeout.Infinite); else TriggerRefresh(); } } public override bool OverrideCatalogZoneTransfer { get { throw new InvalidOperationException(); } set { throw new InvalidOperationException(); } } public override bool OverrideCatalogNotify { get { throw new InvalidOperationException(); } set { throw new InvalidOperationException(); } } public override AuthZoneQueryAccess QueryAccess { get { return base.QueryAccess; } set { switch (value) { case AuthZoneQueryAccess.AllowOnlyZoneNameServers: case AuthZoneQueryAccess.AllowZoneNameServersAndUseSpecifiedNetworkACL: throw new ArgumentException("The Query Access option is invalid for Stub zones: " + value.ToString(), nameof(QueryAccess)); } base.QueryAccess = value; } } public override AuthZoneTransfer ZoneTransfer { get { return base.ZoneTransfer; } set { throw new InvalidOperationException(); } } public override AuthZoneNotify Notify { get { return base.Notify; } set { throw new InvalidOperationException(); } } public override AuthZoneUpdate Update { get { return base.Update; } set { throw new InvalidOperationException(); } } public IReadOnlyList PrimaryNameServerAddresses { get { return _primaryNameServerAddresses; } set { if ((value is null) || (value.Count == 0)) { _primaryNameServerAddresses = null; } else if (value.Count > byte.MaxValue) { throw new ArgumentOutOfRangeException(nameof(PrimaryNameServerAddresses), "Name server addresses cannot have more than 255 entries."); } else { foreach (NameServerAddress nameServer in value) { if (nameServer.Port != 53) throw new ArgumentException("Name server address must use port 53 for Stub zones.", nameof(PrimaryNameServerAddresses)); } _primaryNameServerAddresses = value; } //update catalog zone property if (!Disabled) CatalogZone?.SetPrimaryAddressesProperty(_primaryNameServerAddresses, _name); } } public DateTime Expiry { get { return _expiry; } } public bool IsExpired { get { return _isExpired; } } public override bool IsActive { get { return !Disabled && !_isExpired; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/SubDomainZone.cs ================================================ /* 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.Dns.ResourceRecords; using System.Collections.Generic; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { abstract class SubDomainZone : AuthZone { #region variables readonly ApexZone _authoritativeZone; #endregion #region constructor protected SubDomainZone(ApexZone authoritativeZone, string name) : base(name) { _authoritativeZone = authoritativeZone; } #endregion #region public public void AutoUpdateState() { foreach (KeyValuePair> entry in _entries) { foreach (DnsResourceRecord record in entry.Value) { if (!record.GetAuthGenericRecordInfo().Disabled) { Disabled = false; return; } } } Disabled = true; } #endregion #region properties public ApexZone AuthoritativeZone { get { return _authoritativeZone; } } #endregion } } ================================================ FILE: DnsServerCore/Dns/Zones/Zone.cs ================================================ /* 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 System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.Dns.Zones { abstract class Zone { #region variables protected readonly string _name; protected readonly ConcurrentDictionary> _entries; #endregion #region constructor protected Zone(string name) { _name = name.ToLowerInvariant(); _entries = new ConcurrentDictionary>(1, 5); } protected Zone(string name, int capacity) { _name = name.ToLowerInvariant(); _entries = new ConcurrentDictionary>(1, capacity); } protected Zone(string name, ConcurrentDictionary> entries) { _name = name.ToLowerInvariant(); _entries = entries; } #endregion #region static public static string GetReverseZone(IPAddress address, IPAddress subnetMask) { return GetReverseZone(address, subnetMask.GetSubnetMaskWidth()); } public static string GetReverseZone(IPAddress address, int subnetMaskWidth) { int addressByteCount = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(subnetMaskWidth) / 8)); byte[] addressBytes = address.GetAddressBytes(); string reverseZone = ""; switch (address.AddressFamily) { case AddressFamily.InterNetwork: for (int i = 0; i < addressByteCount; i++) reverseZone = addressBytes[i] + "." + reverseZone; reverseZone += "in-addr.arpa"; break; case AddressFamily.InterNetworkV6: for (int i = 0; i < addressByteCount; i++) reverseZone = (addressBytes[i] & 0x0F).ToString("x") + "." + (addressBytes[i] >> 4).ToString("x") + "." + reverseZone; reverseZone += "ip6.arpa"; break; default: throw new NotSupportedException("AddressFamily not supported."); } return reverseZone; } #endregion #region public public virtual void ListAllRecords(List records) { foreach (KeyValuePair> entry in _entries) records.AddRange(entry.Value); } public abstract bool ContainsNameServerRecords(); public override string ToString() { return _name; } #endregion #region properties public string Name { get { return _name; } } public virtual bool IsEmpty { get { return _entries.IsEmpty; } } #endregion } } ================================================ FILE: DnsServerCore/DnsServerCore.csproj ================================================  net9.0 false true Shreyas Zare Technitium Technitium DNS Server https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer DnsServer 14.3 false ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.ByteTree.dll ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.IO.dll ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Security.OTP.dll PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: DnsServerCore/DnsWebService.cs ================================================ /* 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 } } ================================================ FILE: DnsServerCore/DnsWebServiceException.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; namespace DnsServerCore { public class DnsWebServiceException : Exception { #region constructors public DnsWebServiceException() : base() { } public DnsWebServiceException(string message) : base(message) { } public DnsWebServiceException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore/DnsWebServiceLegacy.cs ================================================ /* 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.Dns; using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Mail; using System.Net.Sockets; using System.Text; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ClientConnection; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore { public partial class DnsWebService { #region legacy config private bool TryLoadOldConfigFile() { string configFile = Path.Combine(_configFolder, "dns.config"); try { using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read)) { if (TryLoadOldConfigFrom(fS)) { _log.Write("Old DNS config file was loaded: " + configFile); return true; } } } catch (FileNotFoundException) { //do nothing } catch (Exception ex) { _log.Write("DNS Server encountered an error while trying to load old DNS config file: " + configFile + "\r\n" + ex.ToString()); } return false; } private bool TryLoadOldConfigFrom(Stream s) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) == "DS") { int version = bR.ReadByte(); ReadOldConfigFrom(bR, version); s.Dispose(); _dnsServer.SaveConfigFileInternal(); return true; } return false; } private void ReadOldConfigFrom(BinaryReader bR, int version) { if ((version >= 28) && (version <= 42)) { ReadConfigFromV42(bR, version); } else if ((version >= 2) && (version <= 27)) { ReadConfigFromV27(bR, version); //new default settings DnsClientConnection.IPv4SourceAddresses = null; DnsClientConnection.IPv6SourceAddresses = null; _dnsServer.EnableUdpSocketPool = Environment.OSVersion.Platform == PlatformID.Win32NT; UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort]; _dnsServer.MaxConcurrentResolutionsPerCore = 100; _dnsServer.DnsApplicationManager.EnableAutomaticUpdate = true; _webServiceEnableHttp3 = _webServiceEnableTls && IsQuicSupported(); _dnsServer.EnableDnsOverHttp3 = _dnsServer.EnableDnsOverHttps && IsQuicSupported(); _webServiceRealIpHeader = "X-Real-IP"; _dnsServer.DnsOverHttpRealIpHeader = "X-Real-IP"; _dnsServer.DefaultResponsiblePerson = null; _dnsServer.AuthZoneManager.UseSoaSerialDateScheme = false; _dnsServer.AuthZoneManager.MinSoaRefresh = 300; _dnsServer.AuthZoneManager.MinSoaRetry = 300; _dnsServer.ZoneTransferAllowedNetworks = null; _dnsServer.NotifyAllowedNetworks = null; _dnsServer.EDnsClientSubnet = false; _dnsServer.EDnsClientSubnetIPv4PrefixLength = 24; _dnsServer.EDnsClientSubnetIPv6PrefixLength = 56; _dnsServer.EDnsClientSubnetIpv4Override = null; _dnsServer.EDnsClientSubnetIpv6Override = null; _dnsServer.QpmLimitBypassList = null; if (_dnsServer.EnableDnsOverUdpProxy || _dnsServer.EnableDnsOverTcpProxy || _dnsServer.EnableDnsOverHttp) { _dnsServer.ReverseProxyNetworkACL = [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("10.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("100.64.0.0"), 10), new NetworkAccessControl(IPAddress.Parse("169.254.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("172.16.0.0"), 12), new NetworkAccessControl(IPAddress.Parse("192.168.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("2000::"), 3, true), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; } _dnsServer.BlockingBypassList = null; _dnsServer.BlockingAnswerTtl = 30; _dnsServer.ResolverConcurrency = 2; _dnsServer.CacheZoneManager.ServeStaleAnswerTtl = CacheZoneManager.SERVE_STALE_ANSWER_TTL; _dnsServer.CacheZoneManager.ServeStaleResetTtl = CacheZoneManager.SERVE_STALE_RESET_TTL; _dnsServer.ServeStaleMaxWaitTime = DnsServer.SERVE_STALE_MAX_WAIT_TIME; _dnsServer.ConcurrentForwarding = true; _dnsServer.ResolverLogManager = _log; _dnsServer.StatsManager.EnableInMemoryStats = false; } else { throw new InvalidDataException("DNS Server config version not supported."); } } private void ReadConfigFromV42(BinaryReader bR, int version) { //web service { _webServiceHttpPort = bR.ReadInt32(); _webServiceTlsPort = bR.ReadInt32(); { 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 = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any }; } } _webServiceEnableTls = bR.ReadBoolean(); if (version >= 33) _webServiceEnableHttp3 = bR.ReadBoolean(); else _webServiceEnableHttp3 = _webServiceEnableTls && IsQuicSupported(); _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 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()); } StartTlsCertificateUpdateTimer(); } CheckAndLoadSelfSignedCertificate(false, false); if (version >= 38) _webServiceRealIpHeader = bR.ReadShortString(); else _webServiceRealIpHeader = "X-Real-IP"; } //dns { //general _dnsServer.ServerDomain = bR.ReadShortString(); { int count = bR.ReadByte(); if (count > 0) { List localEndPoints = new List(count); for (int i = 0; i < count; i++) { IPEndPoint ep = EndPointExtensions.ReadFrom(bR) as IPEndPoint; if (ep.Port == 853) continue; //to avoid validation exception localEndPoints.Add(ep); } _dnsServer.LocalEndPoints = localEndPoints; } else { _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) }; } } if (version >= 34) { DnsClientConnection.IPv4SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR); DnsClientConnection.IPv6SourceAddresses = AuthZoneInfo.ReadNetworkAddressesFrom(bR); } else { DnsClientConnection.IPv4SourceAddresses = null; DnsClientConnection.IPv6SourceAddresses = null; } _dnsServer.AuthZoneManager.DefaultRecordTtl = bR.ReadUInt32(); if (version >= 36) { string rp = bR.ReadString(); if (rp.Length == 0) _dnsServer.DefaultResponsiblePerson = null; else _dnsServer.DefaultResponsiblePerson = new MailAddress(rp); } else { _dnsServer.DefaultResponsiblePerson = null; } if (version >= 33) _dnsServer.AuthZoneManager.UseSoaSerialDateScheme = bR.ReadBoolean(); else _dnsServer.AuthZoneManager.UseSoaSerialDateScheme = false; if (version >= 40) { _dnsServer.AuthZoneManager.MinSoaRefresh = bR.ReadUInt32(); _dnsServer.AuthZoneManager.MinSoaRetry = bR.ReadUInt32(); } else { _dnsServer.AuthZoneManager.MinSoaRefresh = 300; _dnsServer.AuthZoneManager.MinSoaRetry = 300; } if (version >= 33) _dnsServer.ZoneTransferAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR); else _dnsServer.ZoneTransferAllowedNetworks = null; if (version >= 34) _dnsServer.NotifyAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR); else _dnsServer.NotifyAllowedNetworks = null; _dnsServer.DnsApplicationManager.EnableAutomaticUpdate = bR.ReadBoolean(); _dnsServer.PreferIPv6 = bR.ReadBoolean(); if (version >= 42) { _dnsServer.EnableUdpSocketPool = bR.ReadBoolean(); int count = bR.ReadUInt16(); ushort[] socketPoolExcludedPorts = new ushort[count]; for (int i = 0; i < count; i++) socketPoolExcludedPorts[i] = bR.ReadUInt16(); UdpClientConnection.SocketPoolExcludedPorts = socketPoolExcludedPorts; } else { _dnsServer.EnableUdpSocketPool = Environment.OSVersion.Platform == PlatformID.Win32NT; UdpClientConnection.SocketPoolExcludedPorts = [(ushort)_webServiceTlsPort]; } _dnsServer.UdpPayloadSize = bR.ReadUInt16(); _dnsServer.DnssecValidation = bR.ReadBoolean(); if (version >= 29) { _dnsServer.EDnsClientSubnet = bR.ReadBoolean(); _dnsServer.EDnsClientSubnetIPv4PrefixLength = bR.ReadByte(); _dnsServer.EDnsClientSubnetIPv6PrefixLength = bR.ReadByte(); } else { _dnsServer.EDnsClientSubnet = false; _dnsServer.EDnsClientSubnetIPv4PrefixLength = 24; _dnsServer.EDnsClientSubnetIPv6PrefixLength = 56; } if (version >= 35) { if (bR.ReadBoolean()) _dnsServer.EDnsClientSubnetIpv4Override = NetworkAddress.ReadFrom(bR); else _dnsServer.EDnsClientSubnetIpv4Override = null; if (bR.ReadBoolean()) _dnsServer.EDnsClientSubnetIpv6Override = NetworkAddress.ReadFrom(bR); else _dnsServer.EDnsClientSubnetIpv6Override = null; } else { _dnsServer.EDnsClientSubnetIpv4Override = null; _dnsServer.EDnsClientSubnetIpv6Override = null; } if (version >= 42) { { int count = bR.ReadByte(); Dictionary qpmPrefixLimitsIPv4 = new Dictionary(count); for (int i = 0; i < count; i++) qpmPrefixLimitsIPv4.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32())); _dnsServer.QpmPrefixLimitsIPv4 = qpmPrefixLimitsIPv4; } { int count = bR.ReadByte(); Dictionary qpmPrefixLimitsIPv6 = new Dictionary(count); for (int i = 0; i < count; i++) qpmPrefixLimitsIPv6.Add(bR.ReadInt32(), (bR.ReadInt32(), bR.ReadInt32())); _dnsServer.QpmPrefixLimitsIPv6 = qpmPrefixLimitsIPv6; } _dnsServer.QpmLimitSampleMinutes = bR.ReadInt32(); _dnsServer.QpmLimitUdpTruncationPercentage = bR.ReadInt32(); } else { int qpmLimitRequests = bR.ReadInt32(); _ = bR.ReadInt32(); //obsolete qpmLimitErrors int qpmLimitSampleMinutes = bR.ReadInt32(); int qpmLimitIPv4PrefixLength = bR.ReadInt32(); int qpmLimitIPv6PrefixLength = bR.ReadInt32(); _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary() { { qpmLimitIPv4PrefixLength, (qpmLimitRequests, qpmLimitRequests) } }; _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary() { { qpmLimitIPv6PrefixLength, (qpmLimitRequests, qpmLimitRequests) } }; _dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes; _dnsServer.QpmLimitUdpTruncationPercentage = 0; } if (version >= 34) _dnsServer.QpmLimitBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR); else _dnsServer.QpmLimitBypassList = null; _dnsServer.ClientTimeout = bR.ReadInt32(); if (version < 34) { if (_dnsServer.ClientTimeout == 4000) _dnsServer.ClientTimeout = 2000; } _dnsServer.TcpSendTimeout = bR.ReadInt32(); _dnsServer.TcpReceiveTimeout = bR.ReadInt32(); if (version >= 30) { _dnsServer.QuicIdleTimeout = bR.ReadInt32(); _dnsServer.QuicMaxInboundStreams = bR.ReadInt32(); _dnsServer.ListenBacklog = bR.ReadInt32(); } else { _dnsServer.QuicIdleTimeout = 60000; _dnsServer.QuicMaxInboundStreams = 100; _dnsServer.ListenBacklog = 100; } if (version >= 40) _dnsServer.MaxConcurrentResolutionsPerCore = bR.ReadUInt16(); else _dnsServer.MaxConcurrentResolutionsPerCore = 100; //optional protocols if (version >= 32) { _dnsServer.EnableDnsOverUdpProxy = bR.ReadBoolean(); _dnsServer.EnableDnsOverTcpProxy = bR.ReadBoolean(); } else { _dnsServer.EnableDnsOverUdpProxy = false; _dnsServer.EnableDnsOverTcpProxy = false; } _dnsServer.EnableDnsOverHttp = bR.ReadBoolean(); _dnsServer.EnableDnsOverTls = bR.ReadBoolean(); _dnsServer.EnableDnsOverHttps = bR.ReadBoolean(); if (version >= 37) _dnsServer.EnableDnsOverHttp3 = bR.ReadBoolean(); else _dnsServer.EnableDnsOverHttp3 = _dnsServer.EnableDnsOverHttps && IsQuicSupported(); if (version >= 32) { _dnsServer.EnableDnsOverQuic = bR.ReadBoolean(); _dnsServer.DnsOverUdpProxyPort = bR.ReadInt32(); _dnsServer.DnsOverTcpProxyPort = bR.ReadInt32(); _dnsServer.DnsOverHttpPort = bR.ReadInt32(); _dnsServer.DnsOverTlsPort = bR.ReadInt32(); _dnsServer.DnsOverHttpsPort = bR.ReadInt32(); _dnsServer.DnsOverQuicPort = bR.ReadInt32(); } else if (version >= 31) { _dnsServer.EnableDnsOverQuic = bR.ReadBoolean(); _dnsServer.DnsOverHttpPort = bR.ReadInt32(); _dnsServer.DnsOverTlsPort = bR.ReadInt32(); _dnsServer.DnsOverHttpsPort = bR.ReadInt32(); _dnsServer.DnsOverQuicPort = bR.ReadInt32(); } else if (version >= 30) { _ = bR.ReadBoolean(); //removed EnableDnsOverHttpPort80 value _dnsServer.EnableDnsOverQuic = bR.ReadBoolean(); _dnsServer.DnsOverHttpPort = bR.ReadInt32(); _dnsServer.DnsOverTlsPort = bR.ReadInt32(); _dnsServer.DnsOverHttpsPort = bR.ReadInt32(); _dnsServer.DnsOverQuicPort = bR.ReadInt32(); } else { _dnsServer.EnableDnsOverQuic = false; _dnsServer.DnsOverUdpProxyPort = 538; _dnsServer.DnsOverTcpProxyPort = 538; if (_dnsServer.EnableDnsOverHttps) { _dnsServer.EnableDnsOverHttp = true; _dnsServer.DnsOverHttpPort = 80; } else if (_dnsServer.EnableDnsOverHttp) { _dnsServer.DnsOverHttpPort = 8053; } else { _dnsServer.DnsOverHttpPort = 80; } _dnsServer.DnsOverTlsPort = 853; _dnsServer.DnsOverHttpsPort = 443; _dnsServer.DnsOverQuicPort = 853; } if (version >= 39) { _dnsServer.ReverseProxyNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR); } else { if (_dnsServer.EnableDnsOverUdpProxy || _dnsServer.EnableDnsOverTcpProxy || _dnsServer.EnableDnsOverHttp) { _dnsServer.ReverseProxyNetworkACL = [ new NetworkAccessControl(IPAddress.Parse("127.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("10.0.0.0"), 8), new NetworkAccessControl(IPAddress.Parse("100.64.0.0"), 10), new NetworkAccessControl(IPAddress.Parse("169.254.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("172.16.0.0"), 12), new NetworkAccessControl(IPAddress.Parse("192.168.0.0"), 16), new NetworkAccessControl(IPAddress.Parse("2000::"), 3, true), new NetworkAccessControl(IPAddress.IPv6Any, 0) ]; } } string dnsTlsCertificatePath = bR.ReadShortString(); string dnsTlsCertificatePassword = bR.ReadShortString(); if (dnsTlsCertificatePath.Length == 0) dnsTlsCertificatePath = null; if (dnsTlsCertificatePath is null) _dnsServer.RemoveDnsTlsCertificate(); else _dnsServer.SetDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword); if (version >= 38) _dnsServer.DnsOverHttpRealIpHeader = bR.ReadShortString(); else _dnsServer.DnsOverHttpRealIpHeader = "X-Real-IP"; //tsig { int count = bR.ReadByte(); Dictionary tsigKeys = new Dictionary(count); for (int i = 0; i < count; i++) { string keyName = bR.ReadShortString(); string sharedSecret = bR.ReadShortString(); TsigAlgorithm algorithm = (TsigAlgorithm)bR.ReadByte(); tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithm)); } _dnsServer.TsigKeys = tsigKeys; } //recursion _dnsServer.Recursion = (DnsServerRecursion)bR.ReadByte(); if (version >= 37) { _dnsServer.RecursionNetworkACL = AuthZoneInfo.ReadNetworkACLFrom(bR); } else { NetworkAddress[] recursionDeniedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR); NetworkAddress[] recursionAllowedNetworks = AuthZoneInfo.ReadNetworkAddressesFrom(bR); _dnsServer.RecursionNetworkACL = AuthZoneInfo.ConvertDenyAllowToACL(recursionDeniedNetworks, recursionAllowedNetworks); } _dnsServer.RandomizeName = bR.ReadBoolean(); _dnsServer.QnameMinimization = bR.ReadBoolean(); if (version <= 40) _ = bR.ReadBoolean(); //removed NsRevalidation option _dnsServer.ResolverRetries = bR.ReadInt32(); _dnsServer.ResolverTimeout = bR.ReadInt32(); if (version >= 37) _dnsServer.ResolverConcurrency = bR.ReadInt32(); else _dnsServer.ResolverConcurrency = 2; _dnsServer.ResolverMaxStackCount = bR.ReadInt32(); //cache if (version >= 30) _dnsServer.SaveCacheToDisk = bR.ReadBoolean(); else _dnsServer.SaveCacheToDisk = true; _dnsServer.ServeStale = bR.ReadBoolean(); _dnsServer.CacheZoneManager.ServeStaleTtl = bR.ReadUInt32(); if (version >= 36) { _dnsServer.CacheZoneManager.ServeStaleAnswerTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.ServeStaleResetTtl = bR.ReadUInt32(); _dnsServer.ServeStaleMaxWaitTime = bR.ReadInt32(); } else { _dnsServer.CacheZoneManager.ServeStaleAnswerTtl = CacheZoneManager.SERVE_STALE_ANSWER_TTL; _dnsServer.CacheZoneManager.ServeStaleResetTtl = CacheZoneManager.SERVE_STALE_RESET_TTL; _dnsServer.ServeStaleMaxWaitTime = DnsServer.SERVE_STALE_MAX_WAIT_TIME; } _dnsServer.CacheZoneManager.MaximumEntries = bR.ReadInt64(); _dnsServer.CacheZoneManager.MinimumRecordTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.MaximumRecordTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.NegativeRecordTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.FailureRecordTtl = bR.ReadUInt32(); _dnsServer.CachePrefetchEligibility = bR.ReadInt32(); _dnsServer.CachePrefetchTrigger = bR.ReadInt32(); _dnsServer.CachePrefetchSampleIntervalMinutes = bR.ReadInt32(); _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = bR.ReadInt32(); //blocking _dnsServer.EnableBlocking = bR.ReadBoolean(); _dnsServer.AllowTxtBlockingReport = bR.ReadBoolean(); if (version >= 33) _dnsServer.BlockingBypassList = AuthZoneInfo.ReadNetworkAddressesFrom(bR); else _dnsServer.BlockingBypassList = null; _dnsServer.BlockingType = (DnsServerBlockingType)bR.ReadByte(); if (version >= 38) _dnsServer.BlockingAnswerTtl = bR.ReadUInt32(); else _dnsServer.BlockingAnswerTtl = 30; { //read custom blocking addresses int count = bR.ReadByte(); if (count > 0) { List dnsARecords = new List(); List dnsAAAARecords = new List(); for (int i = 0; i < count; i++) { IPAddress customAddress = IPAddressExtensions.ReadFrom(bR); switch (customAddress.AddressFamily) { case AddressFamily.InterNetwork: dnsARecords.Add(new DnsARecordData(customAddress)); break; case AddressFamily.InterNetworkV6: dnsAAAARecords.Add(new DnsAAAARecordData(customAddress)); break; } } _dnsServer.CustomBlockingARecords = dnsARecords; _dnsServer.CustomBlockingAAAARecords = dnsAAAARecords; } else { _dnsServer.CustomBlockingARecords = null; _dnsServer.CustomBlockingAAAARecords = null; } } { //read block list urls int count = bR.ReadByte(); string[] blockListUrls = new string[count]; for (int i = 0; i < count; i++) blockListUrls[i] = bR.ReadShortString(); _dnsServer.BlockListZoneManager.BlockListUrls = blockListUrls; _dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = bR.ReadInt32(); _dnsServer.BlockListZoneManager.BlockListLastUpdatedOn = bR.ReadDateTime(); } //proxy & forwarders NetProxyType proxyType = (NetProxyType)bR.ReadByte(); if (proxyType != NetProxyType.None) { string address = bR.ReadShortString(); int port = bR.ReadInt32(); NetworkCredential credential = null; if (bR.ReadBoolean()) //credential set credential = new NetworkCredential(bR.ReadShortString(), bR.ReadShortString()); _dnsServer.Proxy = NetProxy.CreateProxy(proxyType, address, port, credential); int count = bR.ReadByte(); List bypassList = new List(count); for (int i = 0; i < count; i++) bypassList.Add(new NetProxyBypassItem(bR.ReadShortString())); _dnsServer.Proxy.BypassList = bypassList; } else { _dnsServer.Proxy = null; } { int count = bR.ReadByte(); if (count > 0) { NameServerAddress[] forwarders = new NameServerAddress[count]; for (int i = 0; i < count; i++) { forwarders[i] = new NameServerAddress(bR); if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson) forwarders[i] = forwarders[i].Clone(DnsTransportProtocol.Https); } _dnsServer.Forwarders = forwarders; } else { _dnsServer.Forwarders = null; } } if (version >= 37) _dnsServer.ConcurrentForwarding = bR.ReadBoolean(); else _dnsServer.ConcurrentForwarding = true; _dnsServer.ForwarderRetries = bR.ReadInt32(); _dnsServer.ForwarderTimeout = bR.ReadInt32(); _dnsServer.ForwarderConcurrency = bR.ReadInt32(); //logging if (version >= 33) { if (bR.ReadBoolean()) //ignore resolver logs _dnsServer.ResolverLogManager = null; else _dnsServer.ResolverLogManager = _log; } else { _dnsServer.ResolverLogManager = _log; } if (bR.ReadBoolean()) //log all queries _dnsServer.QueryLogManager = _log; else _dnsServer.QueryLogManager = null; if (version >= 34) _dnsServer.StatsManager.EnableInMemoryStats = bR.ReadBoolean(); else _dnsServer.StatsManager.EnableInMemoryStats = false; { int maxStatFileDays = bR.ReadInt32(); if (maxStatFileDays < 0) maxStatFileDays = 0; _dnsServer.StatsManager.MaxStatFileDays = maxStatFileDays; } } } private void ReadConfigFromV27(BinaryReader bR, int version) { _dnsServer.ServerDomain = bR.ReadShortString(); _webServiceHttpPort = bR.ReadInt32(); if (version >= 13) { { 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 = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any }; } } _webServiceTlsPort = bR.ReadInt32(); _webServiceEnableTls = bR.ReadBoolean(); _webServiceHttpToTlsRedirect = bR.ReadBoolean(); _webServiceTlsCertificatePath = bR.ReadShortString(); _webServiceTlsCertificatePassword = bR.ReadShortString(); if (_webServiceTlsCertificatePath.Length == 0) _webServiceTlsCertificatePath = null; if (_webServiceTlsCertificatePath is null) { StopTlsCertificateUpdateTimer(); } else { string 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()); } StartTlsCertificateUpdateTimer(); } } else { _webServiceLocalAddresses = new IPAddress[] { IPAddress.Any, IPAddress.IPv6Any }; _webServiceTlsPort = 53443; _webServiceEnableTls = false; _webServiceHttpToTlsRedirect = false; _webServiceTlsCertificatePath = string.Empty; _webServiceTlsCertificatePassword = string.Empty; } _dnsServer.PreferIPv6 = bR.ReadBoolean(); if (bR.ReadBoolean()) //logQueries _dnsServer.QueryLogManager = _log; if (version >= 14) { int maxStatFileDays = bR.ReadInt32(); if (maxStatFileDays < 0) maxStatFileDays = 0; _dnsServer.StatsManager.MaxStatFileDays = maxStatFileDays; } else { _dnsServer.StatsManager.MaxStatFileDays = 0; } if (version >= 17) { _dnsServer.Recursion = (DnsServerRecursion)bR.ReadByte(); NetworkAddress[] recursionDeniedNetworks; { int count = bR.ReadByte(); if (count > 0) { NetworkAddress[] networks = new NetworkAddress[count]; for (int i = 0; i < count; i++) networks[i] = NetworkAddress.ReadFrom(bR); recursionDeniedNetworks = networks; } else { recursionDeniedNetworks = null; } } NetworkAddress[] recursionAllowedNetworks; { int count = bR.ReadByte(); if (count > 0) { NetworkAddress[] networks = new NetworkAddress[count]; for (int i = 0; i < count; i++) networks[i] = NetworkAddress.ReadFrom(bR); recursionAllowedNetworks = networks; } else { recursionAllowedNetworks = null; } } _dnsServer.RecursionNetworkACL = AuthZoneInfo.ConvertDenyAllowToACL(recursionDeniedNetworks, recursionAllowedNetworks); } else { bool allowRecursion = bR.ReadBoolean(); bool allowRecursionOnlyForPrivateNetworks; if (version >= 4) allowRecursionOnlyForPrivateNetworks = bR.ReadBoolean(); else allowRecursionOnlyForPrivateNetworks = true; //default true for security reasons if (allowRecursion) { if (allowRecursionOnlyForPrivateNetworks) _dnsServer.Recursion = DnsServerRecursion.AllowOnlyForPrivateNetworks; else _dnsServer.Recursion = DnsServerRecursion.Allow; } else { _dnsServer.Recursion = DnsServerRecursion.Deny; } } if (version >= 12) _dnsServer.RandomizeName = bR.ReadBoolean(); else _dnsServer.RandomizeName = false; //default false to allow resolving from bad name servers if (version >= 15) _dnsServer.QnameMinimization = bR.ReadBoolean(); else _dnsServer.QnameMinimization = true; //default true to enable privacy feature if (version >= 20) { int qpmLimitRequests = bR.ReadInt32(); _ = bR.ReadInt32(); //obsolete qpmLimitErrors int qpmLimitSampleMinutes = bR.ReadInt32(); int qpmLimitIPv4PrefixLength = bR.ReadInt32(); int qpmLimitIPv6PrefixLength = bR.ReadInt32(); _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary() { { qpmLimitIPv4PrefixLength, (qpmLimitRequests, qpmLimitRequests) } }; _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary() { { qpmLimitIPv6PrefixLength, (qpmLimitRequests, qpmLimitRequests) } }; _dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes; _dnsServer.QpmLimitUdpTruncationPercentage = 0; } else if (version >= 17) { int qpmLimitRequests = bR.ReadInt32(); int qpmLimitSampleMinutes = bR.ReadInt32(); _ = bR.ReadInt32(); //read obsolete value _dnsServer.QpmLimitSamplingIntervalInMinutes _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary() { { 24, (qpmLimitRequests, qpmLimitRequests) } }; _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary() { { 56, (qpmLimitRequests, qpmLimitRequests) } }; _dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes; _dnsServer.QpmLimitUdpTruncationPercentage = 0; } else { _dnsServer.QpmPrefixLimitsIPv4 = new Dictionary() { { 32, (600, 600) }, { 24, (6000, 6000) } }; _dnsServer.QpmPrefixLimitsIPv6 = new Dictionary() { { 128, (600, 600) }, { 64, (1200, 1200) }, { 56, (6000, 6000) } }; _dnsServer.QpmLimitSampleMinutes = 5; _dnsServer.QpmLimitUdpTruncationPercentage = 50; } if (version >= 13) { _dnsServer.ServeStale = bR.ReadBoolean(); _dnsServer.CacheZoneManager.ServeStaleTtl = bR.ReadUInt32(); } else { _dnsServer.ServeStale = true; _dnsServer.CacheZoneManager.ServeStaleTtl = CacheZoneManager.SERVE_STALE_TTL; } if (version >= 9) { _dnsServer.CachePrefetchEligibility = bR.ReadInt32(); _dnsServer.CachePrefetchTrigger = bR.ReadInt32(); _dnsServer.CachePrefetchSampleIntervalMinutes = bR.ReadInt32(); _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = bR.ReadInt32(); } else { _dnsServer.CachePrefetchEligibility = 2; _dnsServer.CachePrefetchTrigger = 9; _dnsServer.CachePrefetchSampleIntervalMinutes = 5; _dnsServer.CachePrefetchSampleEligibilityHitsPerHour = 30; } NetProxyType proxyType = (NetProxyType)bR.ReadByte(); if (proxyType != NetProxyType.None) { string address = bR.ReadShortString(); int port = bR.ReadInt32(); NetworkCredential credential = null; if (bR.ReadBoolean()) //credential set credential = new NetworkCredential(bR.ReadShortString(), bR.ReadShortString()); _dnsServer.Proxy = NetProxy.CreateProxy(proxyType, address, port, credential); if (version >= 10) { int count = bR.ReadByte(); List bypassList = new List(count); for (int i = 0; i < count; i++) bypassList.Add(new NetProxyBypassItem(bR.ReadShortString())); _dnsServer.Proxy.BypassList = bypassList; } else { _dnsServer.Proxy.BypassList = null; } } else { _dnsServer.Proxy = null; } { int count = bR.ReadByte(); if (count > 0) { NameServerAddress[] forwarders = new NameServerAddress[count]; for (int i = 0; i < count; i++) { forwarders[i] = new NameServerAddress(bR); if (forwarders[i].Protocol == DnsTransportProtocol.HttpsJson) forwarders[i] = forwarders[i].Clone(DnsTransportProtocol.Https); } _dnsServer.Forwarders = forwarders; } else { _dnsServer.Forwarders = null; } } if (version <= 10) { DnsTransportProtocol forwarderProtocol = (DnsTransportProtocol)bR.ReadByte(); if (forwarderProtocol == DnsTransportProtocol.HttpsJson) forwarderProtocol = DnsTransportProtocol.Https; if (_dnsServer.Forwarders != null) { List forwarders = new List(); foreach (NameServerAddress forwarder in _dnsServer.Forwarders) { if (forwarder.Protocol == forwarderProtocol) forwarders.Add(forwarder); else forwarders.Add(forwarder.Clone(forwarderProtocol)); } _dnsServer.Forwarders = forwarders; } } { int count = bR.ReadByte(); if (count > 0) { if (version > 2) { for (int i = 0; i < count; i++) { string username = bR.ReadShortString(); string passwordHash = bR.ReadShortString(); if (username.Equals("admin", StringComparison.OrdinalIgnoreCase)) { _authManager.LoadOldConfig(passwordHash, true); break; } } } else { for (int i = 0; i < count; i++) { string username = bR.ReadShortString(); string password = bR.ReadShortString(); if (username.Equals("admin", StringComparison.OrdinalIgnoreCase)) { _authManager.LoadOldConfig(password, false); break; } } } } } if (version <= 6) { int count = bR.ReadInt32(); _configDisabledZones = new List(count); for (int i = 0; i < count; i++) { string domain = bR.ReadShortString(); _configDisabledZones.Add(domain); } } if (version >= 18) _dnsServer.EnableBlocking = bR.ReadBoolean(); else _dnsServer.EnableBlocking = true; if (version >= 18) _dnsServer.BlockingType = (DnsServerBlockingType)bR.ReadByte(); else if (version >= 16) _dnsServer.BlockingType = bR.ReadBoolean() ? DnsServerBlockingType.NxDomain : DnsServerBlockingType.AnyAddress; else _dnsServer.BlockingType = DnsServerBlockingType.AnyAddress; if (version >= 18) { //read custom blocking addresses int count = bR.ReadByte(); if (count > 0) { List dnsARecords = new List(); List dnsAAAARecords = new List(); for (int i = 0; i < count; i++) { IPAddress customAddress = IPAddressExtensions.ReadFrom(bR); switch (customAddress.AddressFamily) { case AddressFamily.InterNetwork: dnsARecords.Add(new DnsARecordData(customAddress)); break; case AddressFamily.InterNetworkV6: dnsAAAARecords.Add(new DnsAAAARecordData(customAddress)); break; } } _dnsServer.CustomBlockingARecords = dnsARecords; _dnsServer.CustomBlockingAAAARecords = dnsAAAARecords; } else { _dnsServer.CustomBlockingARecords = null; _dnsServer.CustomBlockingAAAARecords = null; } } else { _dnsServer.CustomBlockingARecords = null; _dnsServer.CustomBlockingAAAARecords = null; } if (version > 4) { //read block list urls int count = bR.ReadByte(); string[] blockListUrls = new string[count]; for (int i = 0; i < count; i++) blockListUrls[i] = bR.ReadShortString(); _dnsServer.BlockListZoneManager.BlockListUrls = blockListUrls; _dnsServer.BlockListZoneManager.BlockListLastUpdatedOn = bR.ReadDateTime(); if (version >= 13) _dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = bR.ReadInt32(); } else { _dnsServer.BlockListZoneManager.BlockListUrls = null; _dnsServer.BlockListZoneManager.BlockListLastUpdatedOn = DateTime.MinValue; _dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = 24; } if (version >= 11) { int count = bR.ReadByte(); if (count > 0) { List localEndPoints = new List(count); for (int i = 0; i < count; i++) { IPEndPoint ep = EndPointExtensions.ReadFrom(bR) as IPEndPoint; if (ep.Port == 853) continue; //to avoid validation exception localEndPoints.Add(ep); } _dnsServer.LocalEndPoints = localEndPoints; } else { _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) }; } } else if (version >= 6) { int count = bR.ReadByte(); if (count > 0) { List localEndPoints = new List(count); for (int i = 0; i < count; i++) { IPEndPoint ep = EndPointExtensions.ReadFrom(bR) as IPEndPoint; if (ep.Port == 853) continue; //to avoid validation exception localEndPoints.Add(ep); } _dnsServer.LocalEndPoints = localEndPoints; } else { _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) }; } } else { _dnsServer.LocalEndPoints = new IPEndPoint[] { new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53) }; } if (version >= 8) { _dnsServer.EnableDnsOverHttp = bR.ReadBoolean(); _dnsServer.EnableDnsOverTls = bR.ReadBoolean(); _dnsServer.EnableDnsOverHttps = bR.ReadBoolean(); string dnsTlsCertificatePath = bR.ReadShortString(); string dnsTlsCertificatePassword = bR.ReadShortString(); if (dnsTlsCertificatePath.Length == 0) dnsTlsCertificatePath = null; if (dnsTlsCertificatePath is null) _dnsServer.RemoveDnsTlsCertificate(); else _dnsServer.SetDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword); } else { _dnsServer.EnableDnsOverHttp = false; _dnsServer.EnableDnsOverTls = false; _dnsServer.EnableDnsOverHttps = false; _dnsServer.RemoveDnsTlsCertificate(); } if (version >= 19) { _dnsServer.CacheZoneManager.MinimumRecordTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.MaximumRecordTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.NegativeRecordTtl = bR.ReadUInt32(); _dnsServer.CacheZoneManager.FailureRecordTtl = bR.ReadUInt32(); } else { _dnsServer.CacheZoneManager.MinimumRecordTtl = CacheZoneManager.MINIMUM_RECORD_TTL; _dnsServer.CacheZoneManager.MaximumRecordTtl = CacheZoneManager.MAXIMUM_RECORD_TTL; _dnsServer.CacheZoneManager.NegativeRecordTtl = CacheZoneManager.NEGATIVE_RECORD_TTL; _dnsServer.CacheZoneManager.FailureRecordTtl = CacheZoneManager.FAILURE_RECORD_TTL; } if (version >= 21) { int count = bR.ReadByte(); Dictionary tsigKeys = new Dictionary(count); for (int i = 0; i < count; i++) { string keyName = bR.ReadShortString(); string sharedSecret = bR.ReadShortString(); TsigAlgorithm algorithm = (TsigAlgorithm)bR.ReadByte(); tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, algorithm)); } _dnsServer.TsigKeys = tsigKeys; } else if (version >= 20) { int count = bR.ReadByte(); Dictionary tsigKeys = new Dictionary(count); for (int i = 0; i < count; i++) { string keyName = bR.ReadShortString(); string sharedSecret = bR.ReadShortString(); tsigKeys.Add(keyName, new TsigKey(keyName, sharedSecret, TsigAlgorithm.HMAC_SHA256)); } _dnsServer.TsigKeys = tsigKeys; } else { _dnsServer.TsigKeys = null; } if (version >= 22) _ = bR.ReadBoolean(); //removed NsRevalidation option if (version >= 23) { _dnsServer.AllowTxtBlockingReport = bR.ReadBoolean(); _dnsServer.AuthZoneManager.DefaultRecordTtl = bR.ReadUInt32(); } else { _dnsServer.AllowTxtBlockingReport = true; _dnsServer.AuthZoneManager.DefaultRecordTtl = 3600; } if (version >= 24) { _webServiceUseSelfSignedTlsCertificate = bR.ReadBoolean(); CheckAndLoadSelfSignedCertificate(false, false); } else { _webServiceUseSelfSignedTlsCertificate = false; } if (version >= 25) _dnsServer.UdpPayloadSize = bR.ReadUInt16(); else _dnsServer.UdpPayloadSize = DnsDatagram.EDNS_DEFAULT_UDP_PAYLOAD_SIZE; if (version >= 26) { _dnsServer.DnssecValidation = bR.ReadBoolean(); _dnsServer.ResolverRetries = bR.ReadInt32(); _dnsServer.ResolverTimeout = bR.ReadInt32(); _dnsServer.ResolverMaxStackCount = bR.ReadInt32(); _dnsServer.ForwarderRetries = bR.ReadInt32(); _dnsServer.ForwarderTimeout = bR.ReadInt32(); _dnsServer.ForwarderConcurrency = bR.ReadInt32(); _dnsServer.ClientTimeout = bR.ReadInt32(); if (_dnsServer.ClientTimeout == 4000) _dnsServer.ClientTimeout = 2000; _dnsServer.TcpSendTimeout = bR.ReadInt32(); _dnsServer.TcpReceiveTimeout = bR.ReadInt32(); } else { _dnsServer.DnssecValidation = true; CreateForwarderZoneToDisableDnssecForNTP(); _dnsServer.ResolverRetries = 2; _dnsServer.ResolverTimeout = 1500; _dnsServer.ResolverMaxStackCount = 16; _dnsServer.ForwarderRetries = 3; _dnsServer.ForwarderTimeout = 2000; _dnsServer.ForwarderConcurrency = 2; _dnsServer.ClientTimeout = 2000; _dnsServer.TcpSendTimeout = 10000; _dnsServer.TcpReceiveTimeout = 10000; } if (version >= 27) _dnsServer.CacheZoneManager.MaximumEntries = bR.ReadInt32(); else _dnsServer.CacheZoneManager.MaximumEntries = 10000; } #endregion } } ================================================ FILE: DnsServerCore/Extensions.cs ================================================ /* 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using System; using System.Net; using System.Text.Json; using TechnitiumLibrary; using TechnitiumLibrary.Net; namespace DnsServerCore { static class Extensions { static readonly string[] HTTP_METHODS = new string[] { "GET", "POST" }; static readonly char[] COMMA_SEPARATOR = new char[] { ',' }; public static IPEndPoint GetRemoteEndPoint(this HttpContext context, string realIpHeaderName = null) { try { IPAddress remoteIP = context.Connection.RemoteIpAddress; if (remoteIP is null) return new IPEndPoint(IPAddress.Any, 0); if (remoteIP.IsIPv4MappedToIPv6) remoteIP = remoteIP.MapToIPv4(); if (!string.IsNullOrEmpty(realIpHeaderName) && NetUtilities.IsPrivateIP(remoteIP)) { //get the real IP address of the requesting client from X-Real-IP header set in nginx proxy_pass block string xRealIp = context.Request.Headers[realIpHeaderName]; if (IPAddress.TryParse(xRealIp, out IPAddress address)) return new IPEndPoint(address, 0); } return new IPEndPoint(remoteIP, context.Connection.RemotePort); } catch { return new IPEndPoint(IPAddress.Any, 0); } } public static IPAddress GetLocalIpAddress(this HttpContext context) { try { IPAddress localIP = context.Connection.LocalIpAddress; if (localIP is null) return IPAddress.Any; if (localIP.IsIPv4MappedToIPv6) localIP = localIP.MapToIPv4(); return localIP; } catch { return IPAddress.Any; } } public static UserSession GetCurrentSession(this HttpContext context) { if (context.Items["session"] is UserSession userSession) return userSession; throw new InvalidOperationException(); } public static Utf8JsonWriter GetCurrentJsonWriter(this HttpContext context) { if (context.Items["jsonWriter"] is Utf8JsonWriter jsonWriter) return jsonWriter; throw new InvalidOperationException(); } public static IEndpointConventionBuilder MapGetAndPost(this IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate) { return endpoints.MapMethods(pattern, HTTP_METHODS, requestDelegate); } public static IEndpointConventionBuilder MapGetAndPost(this IEndpointRouteBuilder endpoints, string pattern, Delegate handler) { return endpoints.MapMethods(pattern, HTTP_METHODS, handler); } public static string QueryOrForm(this HttpRequest request, string parameter) { if (request.HttpContext.Items.TryGetValue("jsonContent", out object jsonObject)) { JsonDocument json = (JsonDocument)jsonObject; if (!json.RootElement.TryGetProperty(parameter, out JsonElement jsonValue)) return null; switch (jsonValue.ValueKind) { case JsonValueKind.String: return jsonValue.GetString(); case JsonValueKind.Number: case JsonValueKind.True: case JsonValueKind.False: return jsonValue.ToString(); case JsonValueKind.Null: return null; default: throw new InvalidOperationException(); } } string value = request.Query[parameter]; if ((value is null) && request.HasFormContentType) value = request.Form[parameter]; return value; } public static string GetQueryOrForm(this HttpRequest request, string parameter) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); return value; } public static string GetQueryOrForm(this HttpRequest request, string parameter, string defaultValue) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) return defaultValue; return value; } public static T GetQueryOrForm(this HttpRequest request, string parameter, Func parse) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); return parse(value); } public static T GetQueryOrFormEnum(this HttpRequest request, string parameter) where T : struct { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); return Enum.Parse(value, true); } public static T GetQueryOrForm(this HttpRequest request, string parameter, Func parse, T defaultValue) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) return defaultValue; return parse(value); } public static T GetQueryOrFormEnum(this HttpRequest request, string parameter, T defaultValue) where T : struct { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) return defaultValue; return Enum.Parse(value, true); } public static bool TryGetQueryOrForm(this HttpRequest request, string parameter, out string value) { value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) return false; return true; } public static bool TryGetQueryOrForm(this HttpRequest request, string parameter, Func parse, out T value) { string strValue = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(strValue)) { value = default; return false; } value = parse(strValue); return true; } public static bool TryGetQueryOrFormEnum(this HttpRequest request, string parameter, out T value) where T : struct { string strValue = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(strValue)) { value = default; return false; } return Enum.TryParse(strValue, true, out value); } public static string GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) { value = request.QueryOrForm(alternateParameter); if (string.IsNullOrEmpty(value)) throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); } return value; } public static string GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, string defaultValue) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) { value = request.QueryOrForm(alternateParameter); if (string.IsNullOrEmpty(value)) return defaultValue; } return value; } public static T GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, Func parse) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) { value = request.QueryOrForm(alternateParameter); if (string.IsNullOrEmpty(value)) throw new DnsWebServiceException("Parameter '" + parameter + "' missing."); } return parse(value); } public static T GetQueryOrFormAlt(this HttpRequest request, string parameter, string alternateParameter, Func parse, T defaultValue) { string value = request.QueryOrForm(parameter); if (string.IsNullOrEmpty(value)) { value = request.QueryOrForm(alternateParameter); if (string.IsNullOrEmpty(value)) return defaultValue; } return parse(value); } public static bool TryGetQueryOrFormArray(this HttpRequest request, string parameter, out string[] array, params char[] separator) { if (request.HttpContext.Items.TryGetValue("jsonContent", out object jsonObject)) { JsonDocument json = (JsonDocument)jsonObject; if (!json.RootElement.TryReadArray(parameter, out array)) return false; if (array is null) array = []; return true; } string value = request.QueryOrForm(parameter); if (value is null) { array = null; return false; } if ((value.Length == 0) || value.Equals("false", StringComparison.OrdinalIgnoreCase)) { array = []; return true; } if ((separator is null) || (separator.Length == 0)) separator = COMMA_SEPARATOR; array = value.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); return true; } public static bool TryGetQueryOrFormArray(this HttpRequest request, string parameter, Func parse, out T[] array, params char[] separator) { if (request.HttpContext.Items.TryGetValue("jsonContent", out object jsonObject)) { JsonDocument json = (JsonDocument)jsonObject; if (!json.RootElement.TryReadArray(parameter, parse, out array)) return false; if (array is null) array = []; return true; } string value = request.QueryOrForm(parameter); if (value is null) { array = null; return false; } if ((value.Length == 0) || value.Equals("false", StringComparison.OrdinalIgnoreCase)) { array = []; return true; } if ((separator is null) || (separator.Length == 0)) separator = COMMA_SEPARATOR; array = value.Split(parse, separator); return true; } public static bool TryGetQueryOrFormArray(this HttpRequest request, string parameter, Func getObject, Func, T> parse, int colspan, out T[] array, params char[] separator) { if (request.HttpContext.Items.TryGetValue("jsonContent", out object jsonObject)) { JsonDocument json = (JsonDocument)jsonObject; if (!json.RootElement.TryReadArray(parameter, getObject, out array)) return false; if (array is null) array = []; return true; } string value = request.QueryOrForm(parameter); if (value is null) { array = null; return false; } if ((value.Length == 0) || value.Equals("false", StringComparison.OrdinalIgnoreCase)) { array = []; return true; } if ((separator is null) || (separator.Length == 0)) separator = COMMA_SEPARATOR; string[] cells = value.Split(separator); array = new T[cells.Length / colspan]; for (int i = 0, j = 0; i < cells.Length; i += colspan) array[j++] = parse(new ArraySegment(cells, i, colspan)); return true; } } } ================================================ FILE: DnsServerCore/InvalidTokenWebServiceException.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; namespace DnsServerCore { public class InvalidTokenWebServiceException : DnsWebServiceException { #region constructors public InvalidTokenWebServiceException() : base() { } public InvalidTokenWebServiceException(string message) : base(message) { } public InvalidTokenWebServiceException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore/LogManager.cs ================================================ /* 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 Microsoft.AspNetCore.Http; using System; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.EDnsOptions; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore { [Flags] public enum LoggingType : byte { None = 0, File = 1, Console = 2, FileAndConsole = 3 } public sealed class LogManager : IDisposable { #region variables static readonly char[] commaSeparator = new char[] { ',' }; readonly string _configFolder; LoggingType _loggingType; string _logFolder; int _maxLogFileDays; bool _useLocalTime; const string LOG_ENTRY_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; const string LOG_FILE_DATE_TIME_FORMAT = "yyyy-MM-dd"; bool _isRunning; string _logFile; StreamWriter _logWriter; DateTime _logDate; readonly object _logFileLock = new object(); Channel _channel; ChannelWriter _channelWriter; Thread _consumerThread; readonly Timer _logCleanupTimer; const int LOG_CLEANUP_TIMER_INITIAL_INTERVAL = 60 * 1000; const int LOG_CLEANUP_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000; readonly object _saveLock = new object(); bool _pendingSave; readonly Timer _saveTimer; const int SAVE_TIMER_INITIAL_INTERVAL = 5000; #endregion #region constructor public LogManager(string configFolder) { _configFolder = configFolder; AppDomain.CurrentDomain.UnhandledException += delegate (object sender, UnhandledExceptionEventArgs e) { string logEntry = GetLogEntry(DateTime.UtcNow, e.ExceptionObject.ToString()); Console.WriteLine(logEntry); if (_loggingType.HasFlag(LoggingType.File)) { lock (_logFileLock) { WriteLogEntry(logEntry); } } }; _logCleanupTimer = new Timer(delegate (object state) { try { if (_maxLogFileDays < 1) return; DateTime cutoffDate = DateTime.UtcNow.AddDays(_maxLogFileDays * -1).Date; DateTimeStyles dateTimeStyles; if (_useLocalTime) dateTimeStyles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal; else dateTimeStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; foreach (string logFile in ListLogFiles()) { string logFileName = Path.GetFileNameWithoutExtension(logFile); if (!DateTime.TryParseExact(logFileName, LOG_FILE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, dateTimeStyles, out DateTime logFileDate)) continue; if (logFileDate < cutoffDate) { try { File.Delete(logFile); Write("LogManager cleanup deleted the log file: " + logFile); } catch (Exception ex) { Write(ex); } } } } catch (Exception ex) { Write(ex); } }); LoadConfigFile(); _saveTimer = new Timer(delegate (object state) { lock (_saveLock) { if (_pendingSave) { try { SaveConfigFileInternal(); _pendingSave = false; } catch (Exception ex) { Write(ex); //set timer to retry again _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } } }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _logCleanupTimer?.Dispose(); StopLogging(); lock (_saveLock) { _saveTimer?.Dispose(); if (_pendingSave) { try { SaveConfigFileInternal(); } catch (Exception ex) { Write(ex); } finally { _pendingSave = false; } } } _disposed = true; GC.SuppressFinalize(this); } #endregion #region config private void LoadConfigFile() { string logConfigFile = Path.Combine(_configFolder, "log.config"); try { using (FileStream fS = new FileStream(logConfigFile, FileMode.Open, FileAccess.Read)) { ReadConfigFrom(fS); } } catch (FileNotFoundException) { _loggingType = LoggingType.File; _logFolder = "logs"; _maxLogFileDays = 365; _useLocalTime = false; SaveConfigFileInternal(); } catch (Exception ex) { //log to console since logger failed to load Console.Write(ex.ToString()); return; } ApplyMaxLogFileDays(); ApplyLoggingType(); } public void LoadConfig(Stream s) { lock (_saveLock) { try { ReadConfigFrom(s); } catch (Exception ex) { //log to console since logger failed to load Console.Write(ex.ToString()); return; } //apply config changes ApplyMaxLogFileDays(); ApplyLoggingType(); //save config file SaveConfigFileInternal(); if (_pendingSave) { _pendingSave = false; _saveTimer.Change(Timeout.Infinite, Timeout.Infinite); } } } private void SaveConfigFileInternal() { string logConfigFile = Path.Combine(_configFolder, "log.config"); using (MemoryStream mS = new MemoryStream()) { //serialize config WriteConfigTo(mS); //write config mS.Position = 0; using (FileStream fS = new FileStream(logConfigFile, FileMode.Create, FileAccess.Write)) { mS.CopyTo(fS); } } Write("DNS Server log config file was saved: " + logConfigFile); } public void SaveConfigFile() { lock (_saveLock) { if (_pendingSave) return; _pendingSave = true; _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); } } private void ReadConfigFrom(Stream s) { BinaryReader bR = new BinaryReader(s); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "LS") //format throw new InvalidDataException("DnsServer log config file format is invalid."); byte version = bR.ReadByte(); switch (version) { case 1: _loggingType = (LoggingType)bR.ReadByte(); _logFolder = bR.ReadShortString(); _maxLogFileDays = bR.ReadInt32(); _useLocalTime = bR.ReadBoolean(); break; default: throw new InvalidDataException("DnsServer log config version not supported."); } } private void WriteConfigTo(Stream s) { BinaryWriter bW = new BinaryWriter(s); bW.Write(Encoding.ASCII.GetBytes("LS")); //format bW.Write((byte)1); //version bW.Write((byte)_loggingType); bW.WriteShortString(_logFolder); bW.Write(_maxLogFileDays); bW.Write(_useLocalTime); } #endregion #region private private void StartLogging() { if (_isRunning) return; UnboundedChannelOptions options = new UnboundedChannelOptions(); options.SingleReader = true; _channel = Channel.CreateUnbounded(options); _channelWriter = _channel.Writer; if (_loggingType.HasFlag(LoggingType.File)) { lock (_logFileLock) { StartNewLogFile(); } } _isRunning = true; //start consumer thread _consumerThread = new Thread(async delegate () { try { await foreach (LogQueueItem item in _channel.Reader.ReadAllAsync()) { if (!_isRunning) break; DateTime dateTime = _useLocalTime ? item._dateTime.ToLocalTime() : item._dateTime; string logEntry = GetLogEntry(dateTime, item._message); if (_loggingType.HasFlag(LoggingType.Console)) Console.WriteLine(logEntry); if (_loggingType.HasFlag(LoggingType.File)) { lock (_logFileLock) { if (dateTime.Date > _logDate) StartNewLogFile(); WriteLogEntry(logEntry); } } } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } }); _consumerThread.Name = "Log"; _consumerThread.IsBackground = true; _consumerThread.Start(); } private void StopLogging() { if (!_isRunning) return; _channelWriter?.TryComplete(); lock (_logFileLock) { CloseCurrentLogFile(); } _isRunning = false; } internal void BulkManipulateLogFiles(Action action) { lock (_logFileLock) { CloseCurrentLogFile(); try { action(); } finally { if (_loggingType.HasFlag(LoggingType.File)) StartNewLogFile(); } } } private void ApplyLoggingType() { if (_isRunning) { //running if (_loggingType == LoggingType.None) { //no logging enabled StopLogging(); } else if (_loggingType.HasFlag(LoggingType.File)) { //file logging is enabled if ((_logWriter is null) || !_logFile.StartsWith(ConvertToAbsolutePath(_logFolder))) { //file not being logged or log folder changed; start new log file lock (_logFileLock) { StartNewLogFile(); } } } else { //only console logging enabled; close open log file, if any if (_logWriter is not null) { lock (_logFileLock) { CloseCurrentLogFile(); } } } } else { //stopped if (_loggingType != LoggingType.None) StartLogging(); } } private void ApplyLogFolder() { Directory.CreateDirectory(ConvertToAbsolutePath(_logFolder)); if (_loggingType.HasFlag(LoggingType.File)) { lock (_logFileLock) { StartNewLogFile(); } } } private void ApplyMaxLogFileDays() { if (_maxLogFileDays > 0) _logCleanupTimer.Change(LOG_CLEANUP_TIMER_INITIAL_INTERVAL, LOG_CLEANUP_TIMER_PERIODIC_INTERVAL); else _logCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite); } 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.IsPathRooted(path)) return path; return Path.Combine(_configFolder, path); } private void StartNewLogFile() { CloseCurrentLogFile(); string logFolder = ConvertToAbsolutePath(_logFolder); if (!Directory.Exists(logFolder)) Directory.CreateDirectory(logFolder); DateTime logStartDateTime = _useLocalTime ? DateTime.Now : DateTime.UtcNow; _logFile = Path.Combine(logFolder, logStartDateTime.ToString(LOG_FILE_DATE_TIME_FORMAT) + ".log"); _logWriter = new StreamWriter(new FileStream(_logFile, FileMode.Append, FileAccess.Write, FileShare.Read)); _logDate = logStartDateTime.Date; WriteLogEntry(GetLogEntry(logStartDateTime, "Logging started.")); } private void CloseCurrentLogFile() { if (_logWriter is not null) { WriteLogEntry(GetLogEntry(DateTime.UtcNow, "Logging stopped.")); _logWriter.Dispose(); _logWriter = null; } } private string GetLogEntry(DateTime dateTime, string message) { string logEntry; if (_useLocalTime) { if (dateTime.Kind == DateTimeKind.Local) logEntry = "[" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " Local] " + message; else logEntry = "[" + dateTime.ToLocalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " Local] " + message; } else { if (dateTime.Kind == DateTimeKind.Utc) logEntry = "[" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " UTC] " + message; else logEntry = "[" + dateTime.ToUniversalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " UTC] " + message; } return logEntry; } private void WriteLogEntry(string logEntry) { if (_logWriter is not null) { _logWriter.WriteLine(logEntry); _logWriter.Flush(); } } #endregion #region public public string[] ListLogFiles() { return Directory.GetFiles(ConvertToAbsolutePath(_logFolder), "*.log", SearchOption.TopDirectoryOnly); } public async Task DownloadLogFileAsync(HttpContext context, string logName, long limit) { string logFileName = logName + ".log"; using (FileStream fS = new FileStream(Path.Combine(ConvertToAbsolutePath(_logFolder), logFileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 64 * 1024, true)) { HttpResponse response = context.Response; response.ContentType = "text/plain"; response.Headers.ContentDisposition = "attachment;filename=" + logFileName; if ((limit > fS.Length) || (limit < 1)) limit = fS.Length; OffsetStream oFS = new OffsetStream(fS, 0, limit); HttpRequest request = context.Request; Stream s; string acceptEncoding = request.Headers.AcceptEncoding; if (string.IsNullOrEmpty(acceptEncoding)) { s = response.Body; } else { string[] acceptEncodingParts = acceptEncoding.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (acceptEncodingParts.Contains("br")) { response.Headers.ContentEncoding = "br"; s = new BrotliStream(response.Body, CompressionMode.Compress); } else if (acceptEncodingParts.Contains("gzip")) { response.Headers.ContentEncoding = "gzip"; s = new GZipStream(response.Body, CompressionMode.Compress); } else if (acceptEncodingParts.Contains("deflate")) { response.Headers.ContentEncoding = "deflate"; s = new DeflateStream(response.Body, CompressionMode.Compress); } else { s = response.Body; } } await using (s) { await oFS.CopyToAsync(s); if (fS.Length > limit) await s.WriteAsync(Encoding.UTF8.GetBytes("\r\n####___TRUNCATED___####")); } } } public void DeleteLogFile(string logName) { string logFile = Path.Combine(ConvertToAbsolutePath(_logFolder), logName + ".log"); if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase)) DeleteCurrentLogFile(); else File.Delete(logFile); } public void DeleteAllLogFiles() { string[] logFiles = ListLogFiles(); foreach (string logFile in logFiles) { if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase)) DeleteCurrentLogFile(); else File.Delete(logFile); } } public void Write(Exception ex) { Write(ex.ToString()); } public void Write(IPEndPoint ep, Exception ex) { Write(ep, ex.ToString()); } public void Write(IPEndPoint ep, string message) { string ipInfo; if (ep == null) ipInfo = ""; else if (ep.Address.IsIPv4MappedToIPv6) ipInfo = "[" + ep.Address.MapToIPv4().ToString() + ":" + ep.Port + "] "; else ipInfo = "[" + ep.ToString() + "] "; Write(ipInfo + message); } public void Write(IPEndPoint ep, DnsTransportProtocol protocol, Exception ex) { Write(ep, protocol, ex.ToString()); } public void Write(IPEndPoint ep, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response) { DnsQuestionRecord q = null; if (request.Question.Count > 0) q = request.Question[0]; string requestInfo; if (q is null) requestInfo = "MISSING QUESTION!"; else requestInfo = "QNAME: " + q.Name + "; QTYPE: " + q.Type.ToString() + "; QCLASS: " + q.Class; if (request.Additional.Count > 0) { DnsResourceRecord lastRR = request.Additional[request.Additional.Count - 1]; if ((lastRR.Type == DnsResourceRecordType.TSIG) && (lastRR.RDATA is DnsTSIGRecordData tsig)) requestInfo += "; TSIG KeyName: " + lastRR.Name.ToLowerInvariant() + "; TSIG Algo: " + tsig.AlgorithmName + "; TSIG Error: " + tsig.Error.ToString(); } string responseInfo; if (response is null) { responseInfo = "; NO RESPONSE FROM SERVER!"; } else { responseInfo = "; RCODE: " + response.RCODE.ToString(); string answer; if (response.Answer.Count == 0) { if (response.Truncation) answer = "[TRUNCATED]"; else answer = "[]"; } else if ((response.Answer.Count > 2) && response.IsZoneTransfer) { answer = "[ZONE TRANSFER]"; } else { answer = "["; for (int i = 0; i < response.Answer.Count; i++) { if (i > 0) answer += ", "; answer += response.Answer[i].RDATA.ToString(); } answer += "]"; if (response.Additional.Count > 0) { switch (q.Type) { case DnsResourceRecordType.NS: case DnsResourceRecordType.MX: case DnsResourceRecordType.SRV: answer += "; ADDITIONAL: ["; for (int i = 0; i < response.Additional.Count; i++) { DnsResourceRecord additional = response.Additional[i]; switch (additional.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: if (i > 0) answer += ", "; answer += additional.Name + " (" + additional.RDATA.ToString() + ")"; break; } } answer += "]"; break; } } } EDnsClientSubnetOptionData responseECS = response.GetEDnsClientSubnetOption(); if (responseECS is not null) answer += "; ECS: " + responseECS.Address.ToString() + "/" + responseECS.ScopePrefixLength; responseInfo += "; ANSWER: " + answer; } Write(ep, protocol, requestInfo + responseInfo); } public void Write(IPEndPoint ep, DnsTransportProtocol protocol, string message) { Write(ep, protocol.ToString(), message); } public void Write(IPEndPoint ep, string protocol, string message) { string ipInfo; if (ep == null) ipInfo = ""; else if (ep.Address.IsIPv4MappedToIPv6) ipInfo = "[" + ep.Address.MapToIPv4().ToString() + ":" + ep.Port + "] "; else ipInfo = "[" + ep.ToString() + "] "; Write(ipInfo + "[" + protocol.ToUpper() + "] " + message); } public void Write(string message) { if (_loggingType != LoggingType.None) _channelWriter?.TryWrite(new LogQueueItem(message)); } public void DeleteCurrentLogFile() { lock (_logFileLock) { CloseCurrentLogFile(); File.Delete(_logFile); if (_loggingType.HasFlag(LoggingType.File)) StartNewLogFile(); } } #endregion #region properties public LoggingType LoggingType { get { return _loggingType; } set { if (_loggingType != value) { _loggingType = value; ApplyLoggingType(); } } } public string LogFolder { get { return _logFolder; } set { string logFolder; if (string.IsNullOrEmpty(value)) logFolder = "logs"; else if (value.Length > 255) throw new ArgumentException("Log folder path length cannot exceed 255 characters.", nameof(LogFolder)); else logFolder = value; string relativeLogFolder = ConvertToRelativePath(logFolder); if (!relativeLogFolder.Equals(_logFolder, StringComparison.Ordinal)) { _logFolder = relativeLogFolder; ApplyLogFolder(); } } } public int MaxLogFileDays { get { return _maxLogFileDays; } set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(MaxLogFileDays), "MaxLogFileDays must be greater than or equal to 0."); if (_maxLogFileDays != value) { _maxLogFileDays = value; ApplyMaxLogFileDays(); } } } public bool UseLocalTime { get { return _useLocalTime; } set { _useLocalTime = value; } } public string CurrentLogFile { get { return _logFile; } } public string LogFolderAbsolutePath { get { return ConvertToAbsolutePath(_logFolder); } } #endregion readonly struct LogQueueItem { #region variables public readonly DateTime _dateTime; public readonly string _message; #endregion #region constructor public LogQueueItem(string message) { _dateTime = DateTime.UtcNow; _message = message; } #endregion } } } ================================================ FILE: DnsServerCore/TwoFactorAuthRequiredWebServiceException.cs ================================================ /* 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 System; namespace DnsServerCore { public class TwoFactorAuthRequiredWebServiceException : InvalidTokenWebServiceException { #region constructors public TwoFactorAuthRequiredWebServiceException() : base() { } public TwoFactorAuthRequiredWebServiceException(string message) : base(message) { } public TwoFactorAuthRequiredWebServiceException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore/WebServiceApi.cs ================================================ /* 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.Dns; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.Zones; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Http.Client; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore { public partial class DnsWebService { class WebServiceApi { #region variables static readonly char[] _domainTrimChars = new char[] { '\t', ' ', '.' }; readonly DnsWebService _dnsWebService; readonly Uri _updateCheckUri; string _checkForUpdateJsonData; DateTime _checkForUpdateJsonDataUpdatedOn; const int CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS = 3600; #endregion #region constructor public WebServiceApi(DnsWebService dnsWebService, Uri updateCheckUri) { _dnsWebService = dnsWebService; _updateCheckUri = updateCheckUri; } #endregion #region private private async Task GetCheckForUpdateJsonData() { if ((_checkForUpdateJsonData is null) || (DateTime.UtcNow > _checkForUpdateJsonDataUpdatedOn.AddSeconds(CHECK_FOR_UPDATE_JSON_DATA_CACHE_TIME_SECONDS))) { HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = _dnsWebService._dnsServer.Proxy; handler.NetworkType = _dnsWebService._dnsServer.PreferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = _dnsWebService._dnsServer; using (HttpClient http = new HttpClient(handler)) { _checkForUpdateJsonData = await http.GetStringAsync(_updateCheckUri); _checkForUpdateJsonDataUpdatedOn = DateTime.UtcNow; } } return _checkForUpdateJsonData; } #endregion #region public public async Task CheckForUpdateAsync(HttpContext context) { Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); if (_updateCheckUri is null) { jsonWriter.WriteBoolean("updateAvailable", false); return; } try { string jsonData = await GetCheckForUpdateJsonData(); using JsonDocument jsonDocument = JsonDocument.Parse(jsonData); JsonElement jsonResponse = jsonDocument.RootElement; string updateVersion = jsonResponse.GetProperty("updateVersion").GetString(); string updateTitle = jsonResponse.GetPropertyValue("updateTitle", null); string updateMessage = jsonResponse.GetPropertyValue("updateMessage", null); string downloadLink = jsonResponse.GetPropertyValue("downloadLink", null); string instructionsLink = jsonResponse.GetPropertyValue("instructionsLink", null); string changeLogLink = jsonResponse.GetPropertyValue("changeLogLink", null); bool updateAvailable = new Version(updateVersion) > _dnsWebService._currentVersion; jsonWriter.WriteBoolean("updateAvailable", updateAvailable); jsonWriter.WriteString("updateVersion", updateVersion); jsonWriter.WriteString("currentVersion", _dnsWebService.GetServerVersion()); if (updateAvailable) { jsonWriter.WriteString("updateTitle", updateTitle); jsonWriter.WriteString("updateMessage", updateMessage); jsonWriter.WriteString("downloadLink", downloadLink); jsonWriter.WriteString("instructionsLink", instructionsLink); jsonWriter.WriteString("changeLogLink", changeLogLink); } string strLog = "Check for update was done {updateAvailable: " + updateAvailable + "; updateVersion: " + updateVersion + ";"; if (!string.IsNullOrEmpty(updateTitle)) strLog += " updateTitle: " + updateTitle + ";"; if (!string.IsNullOrEmpty(updateMessage)) strLog += " updateMessage: " + updateMessage + ";"; if (!string.IsNullOrEmpty(downloadLink)) strLog += " downloadLink: " + downloadLink + ";"; if (!string.IsNullOrEmpty(instructionsLink)) strLog += " instructionsLink: " + instructionsLink + ";"; if (!string.IsNullOrEmpty(changeLogLink)) strLog += " changeLogLink: " + changeLogLink + ";"; strLog += "}"; _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), strLog); } catch (Exception ex) { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "Check for update was done {updateAvailable: False;}\r\n" + ex.ToString()); jsonWriter.WriteBoolean("updateAvailable", false); } } public async Task ResolveQueryAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DnsClient, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string server = request.GetQueryOrForm("server"); string domain = request.GetQueryOrForm("domain").Trim(_domainTrimChars); DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); bool dnssecValidation = request.GetQueryOrForm("dnssec", bool.Parse, false); NetworkAddress eDnsClientSubnet = request.GetQueryOrForm("eDnsClientSubnet", NetworkAddress.Parse, null); if (eDnsClientSubnet is not null) { switch (eDnsClientSubnet.AddressFamily) { case AddressFamily.InterNetwork: if (eDnsClientSubnet.PrefixLength == 32) eDnsClientSubnet = new NetworkAddress(eDnsClientSubnet.Address, 24); break; case AddressFamily.InterNetworkV6: if (eDnsClientSubnet.PrefixLength == 128) eDnsClientSubnet = new NetworkAddress(eDnsClientSubnet.Address, 56); break; } } bool importResponse = request.GetQueryOrForm("import", bool.Parse, false); NetProxy proxy = _dnsWebService._dnsServer.Proxy; bool preferIPv6 = _dnsWebService._dnsServer.PreferIPv6; ushort udpPayloadSize = _dnsWebService._dnsServer.UdpPayloadSize; bool randomizeName = false; bool qnameMinimization = _dnsWebService._dnsServer.QnameMinimization; const int RETRIES = 1; const int TIMEOUT = 10000; DnsDatagram dnsResponse; List rawResponses = new List(); string dnssecErrorMessage = null; if (server.Equals("recursive-resolver", StringComparison.OrdinalIgnoreCase)) { if (type == DnsResourceRecordType.AXFR) throw new DnsServerException("Cannot do zone transfer (AXFR) for 'recursive-resolver'."); DnsQuestionRecord question; if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress address)) question = new DnsQuestionRecord(address, DnsClass.IN); else question = new DnsQuestionRecord(domain, type, DnsClass.IN); DnsCache dnsCache = new DnsCache(); dnsCache.MinimumRecordTtl = 0; dnsCache.MaximumRecordTtl = 7 * 24 * 60 * 60; try { dnsResponse = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(async delegate (CancellationToken cancellationToken1) { return await DnsClient.RecursiveResolveAsync(question, dnsCache, proxy, preferIPv6, udpPayloadSize, randomizeName, qnameMinimization, dnssecValidation, eDnsClientSubnet, RETRIES, TIMEOUT, rawResponses: rawResponses, cancellationToken: cancellationToken1); }, DnsServer.RECURSIVE_RESOLUTION_TIMEOUT); } catch (DnsClientResponseDnssecValidationException ex) { if (ex.InnerException is DnsClientResponseDnssecValidationException ex1) ex = ex1; dnsResponse = ex.Response; dnssecErrorMessage = ex.Message; importResponse = false; } } else if (server.Equals("system-dns", StringComparison.OrdinalIgnoreCase)) { DnsClient dnsClient = new DnsClient(); dnsClient.Proxy = proxy; dnsClient.PreferIPv6 = preferIPv6; dnsClient.RandomizeName = randomizeName; dnsClient.Retries = RETRIES; dnsClient.Timeout = TIMEOUT; dnsClient.UdpPayloadSize = udpPayloadSize; dnsClient.DnssecValidation = dnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; try { dnsResponse = await dnsClient.ResolveAsync(domain, type); } catch (DnsClientResponseDnssecValidationException ex) { if (ex.InnerException is DnsClientResponseDnssecValidationException ex1) ex = ex1; dnsResponse = ex.Response; dnssecErrorMessage = ex.Message; importResponse = false; } } else { if ((type == DnsResourceRecordType.AXFR) && (protocol == DnsTransportProtocol.Udp)) protocol = DnsTransportProtocol.Tcp; NameServerAddress nameServer; if (server.Equals("this-server", StringComparison.OrdinalIgnoreCase)) { switch (protocol) { case DnsTransportProtocol.Udp: nameServer = _dnsWebService._dnsServer.ThisServer; break; case DnsTransportProtocol.Tcp: nameServer = _dnsWebService._dnsServer.ThisServer.Clone(DnsTransportProtocol.Tcp); break; case DnsTransportProtocol.Tls: throw new DnsServerException("Cannot use DNS-over-TLS protocol for 'this-server'. Please use the TLS certificate domain name as the server."); case DnsTransportProtocol.Https: throw new DnsServerException("Cannot use DNS-over-HTTPS protocol for 'this-server'. Please use the TLS certificate domain name with a url as the server."); case DnsTransportProtocol.Quic: throw new DnsServerException("Cannot use DNS-over-QUIC protocol for 'this-server'. Please use the TLS certificate domain name as the server."); default: throw new NotSupportedException("DNS transport protocol is not supported: " + protocol.ToString()); } proxy = null; //no proxy required for this server } else { nameServer = NameServerAddress.Parse(server); if (nameServer.Protocol != protocol) nameServer = nameServer.Clone(protocol); if (nameServer.IsIPEndPointStale) await nameServer.ResolveIPAddressAsync(_dnsWebService._dnsServer, _dnsWebService._dnsServer.PreferIPv6); if ((nameServer.DomainEndPoint is null) && ((protocol == DnsTransportProtocol.Udp) || (protocol == DnsTransportProtocol.Tcp))) { try { await nameServer.ResolveDomainNameAsync(_dnsWebService._dnsServer); } catch { } } } DnsClient dnsClient = new DnsClient(nameServer); dnsClient.Proxy = proxy; dnsClient.PreferIPv6 = preferIPv6; dnsClient.RandomizeName = randomizeName; dnsClient.Retries = RETRIES; dnsClient.Timeout = TIMEOUT; dnsClient.UdpPayloadSize = udpPayloadSize; dnsClient.DnssecValidation = dnssecValidation; dnsClient.EDnsClientSubnet = eDnsClientSubnet; if (dnssecValidation) { if ((type == DnsResourceRecordType.PTR) && IPAddress.TryParse(domain, out IPAddress ptrIp)) domain = ptrIp.GetReverseDomain(); //load trust anchors into dns client if domain is locally hosted _dnsWebService._dnsServer.AuthZoneManager.LoadTrustAnchorsTo(dnsClient, domain, type); } try { dnsResponse = await dnsClient.ResolveAsync(domain, type); } catch (DnsClientResponseDnssecValidationException ex) { if (ex.InnerException is DnsClientResponseDnssecValidationException ex1) ex = ex1; dnsResponse = ex.Response; dnssecErrorMessage = ex.Message; importResponse = false; } if (type == DnsResourceRecordType.AXFR) dnsResponse = dnsResponse.Join(); } if (importResponse) { bool isZoneImport = false; if (type == DnsResourceRecordType.AXFR) { isZoneImport = true; } else { foreach (DnsResourceRecord record in dnsResponse.Answer) { if (record.Type == DnsResourceRecordType.SOA) { if (record.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) isZoneImport = true; break; } } } AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(domain); if ( (zoneInfo is null) || ((zoneInfo.Type != AuthZoneType.Primary) && (zoneInfo.Type != AuthZoneType.Forwarder) && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) || (isZoneImport && !zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase)) ) { if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(domain); if (zoneInfo is null) throw new DnsServerException("Cannot import records: failed to create primary zone."); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); } else { if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); switch (zoneInfo.Type) { case AuthZoneType.Primary: break; case AuthZoneType.Forwarder: if (type == DnsResourceRecordType.AXFR) throw new DnsServerException("Cannot import records via zone transfer: import zone must be of primary type."); break; default: throw new DnsServerException("Cannot import records: import zone must be of primary or forwarder type."); } } if (type == DnsResourceRecordType.AXFR) { _dnsWebService._dnsServer.AuthZoneManager.SyncZoneTransferRecords(zoneInfo.Name, dnsResponse.Answer); } else { List importRecords = new List(dnsResponse.Answer.Count + dnsResponse.Authority.Count); foreach (DnsResourceRecord record in dnsResponse.Answer) { if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0)) { record.RemoveExpiry(); record.Tag = null; //remove cache zone record info importRecords.Add(record); if (record.Type == DnsResourceRecordType.NS) record.SyncGlueRecords(dnsResponse.Additional); } } foreach (DnsResourceRecord record in dnsResponse.Authority) { if (record.Name.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || record.Name.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase) || (zoneInfo.Name.Length == 0)) { record.RemoveExpiry(); record.Tag = null; //remove cache zone record info importRecords.Add(record); if (record.Type == DnsResourceRecordType.NS) record.SyncGlueRecords(dnsResponse.Additional); } } _dnsWebService._dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords, true, true); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS Client imported record(s) for authoritative zone {server: " + server + "; zone: " + zoneInfo.DisplayName + "; type: " + type + ";}"); } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); if (dnssecErrorMessage is not null) jsonWriter.WriteString("warningMessage", dnssecErrorMessage); jsonWriter.WritePropertyName("result"); dnsResponse.SerializeTo(jsonWriter); jsonWriter.WritePropertyName("rawResponses"); jsonWriter.WriteStartArray(); for (int i = 0; i < rawResponses.Count; i++) rawResponses[i].SerializeTo(jsonWriter); jsonWriter.WriteEndArray(); } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceAppsApi.cs ================================================ /* 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.ApplicationCommon; using DnsServerCore.Auth; using DnsServerCore.Dns.Applications; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace DnsServerCore { public partial class DnsWebService { sealed class WebServiceAppsApi { #region variables readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceAppsApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region private private void WriteAppAsJson(Utf8JsonWriter jsonWriter, DnsApplication application, JsonElement jsonStoreAppsArray = default) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", application.Name); jsonWriter.WriteString("description", application.Description); jsonWriter.WriteString("version", DnsWebService.GetCleanVersion(application.Version)); if (jsonStoreAppsArray.ValueKind != JsonValueKind.Undefined) { foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { string name = jsonStoreApp.GetProperty("name").GetString(); if (name.Equals(application.Name)) { string version = null; string url = null; Version storeAppVersion = null; Version lastServerVersion = null; foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; version = jsonVersion.GetProperty("version").GetString(); url = jsonVersion.GetProperty("url").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; } if (storeAppVersion is null) break; //no compatible update available jsonWriter.WriteString("updateVersion", version); jsonWriter.WriteString("updateUrl", url); jsonWriter.WriteBoolean("updateAvailable", storeAppVersion > application.Version); break; } } } jsonWriter.WritePropertyName("dnsApps"); { jsonWriter.WriteStartArray(); foreach (KeyValuePair dnsApp in application.DnsApplications) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("classPath", dnsApp.Key); jsonWriter.WriteString("description", dnsApp.Value.Description); if (dnsApp.Value is IDnsAppRecordRequestHandler appRecordHandler) { jsonWriter.WriteBoolean("isAppRecordRequestHandler", true); jsonWriter.WriteString("recordDataTemplate", appRecordHandler.ApplicationRecordDataTemplate); } else { jsonWriter.WriteBoolean("isAppRecordRequestHandler", false); } jsonWriter.WriteBoolean("isRequestController", dnsApp.Value is IDnsRequestController); jsonWriter.WriteBoolean("isAuthoritativeRequestHandler", dnsApp.Value is IDnsAuthoritativeRequestHandler); jsonWriter.WriteBoolean("isRequestBlockingHandler", dnsApp.Value is IDnsRequestBlockingHandler); jsonWriter.WriteBoolean("isQueryLogger", dnsApp.Value is IDnsQueryLogger); jsonWriter.WriteBoolean("isQueryLogs", dnsApp.Value is IDnsQueryLogs); jsonWriter.WriteBoolean("isPostProcessor", dnsApp.Value is IDnsPostProcessor); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } jsonWriter.WriteEndObject(); } #endregion #region public public async Task ListInstalledAppsAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if ( !_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.View) && !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View) && !_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View) ) { throw new DnsWebServiceException("Access was denied."); } List apps = new List(_dnsWebService._dnsServer.DnsApplicationManager.Applications.Keys); apps.Sort(); JsonDocument jsonDocument = null; try { JsonElement jsonStoreAppsArray = default; if (apps.Count > 0) { try { string storeAppsJsonData = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return _dnsWebService._dnsServer.DnsApplicationManager.GetStoreAppsJsonData(); }, 5000); jsonDocument = JsonDocument.Parse(storeAppsJsonData); jsonStoreAppsArray = jsonDocument.RootElement; } catch (Exception ex) { _dnsWebService._log.Write(ex); } } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("apps"); jsonWriter.WriteStartArray(); foreach (string app in apps) { if (_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(app, out DnsApplication application)) WriteAppAsJson(jsonWriter, application, jsonStoreAppsArray); } jsonWriter.WriteEndArray(); } finally { if (jsonDocument is not null) jsonDocument.Dispose(); } } public async Task ListStoreApps(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); string storeAppsJsonData = await TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return _dnsWebService._dnsServer.DnsApplicationManager.GetStoreAppsJsonData(); }, 30000); using JsonDocument jsonDocument = JsonDocument.Parse(storeAppsJsonData); JsonElement jsonStoreAppsArray = jsonDocument.RootElement; Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("storeApps"); jsonWriter.WriteStartArray(); foreach (JsonElement jsonStoreApp in jsonStoreAppsArray.EnumerateArray()) { string name = jsonStoreApp.GetProperty("name").GetString(); string description = jsonStoreApp.GetProperty("description").GetString(); string version = null; string url = null; string size = null; Version storeAppVersion = null; Version lastServerVersion = null; foreach (JsonElement jsonVersion in jsonStoreApp.GetProperty("versions").EnumerateArray()) { string strServerVersion = jsonVersion.GetProperty("serverVersion").GetString(); Version requiredServerVersion = new Version(strServerVersion); if (_dnsWebService._currentVersion < requiredServerVersion) continue; if ((lastServerVersion is not null) && (lastServerVersion > requiredServerVersion)) continue; version = jsonVersion.GetProperty("version").GetString(); url = jsonVersion.GetProperty("url").GetString(); size = jsonVersion.GetProperty("size").GetString(); storeAppVersion = new Version(version); lastServerVersion = requiredServerVersion; } if (storeAppVersion is null) continue; //app is not compatible jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", name); jsonWriter.WriteString("description", description); jsonWriter.WriteString("version", version); jsonWriter.WriteString("url", url); jsonWriter.WriteString("size", size); bool installed = _dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication installedApp); jsonWriter.WriteBoolean("installed", installed); if (installed) { jsonWriter.WriteString("installedVersion", DnsWebService.GetCleanVersion(installedApp.Version)); jsonWriter.WriteBoolean("updateAvailable", storeAppVersion > installedApp.Version); } jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public async Task DownloadAndInstallAppAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); string url = request.GetQueryOrForm("url"); if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Parameter 'url' value must start with 'https://'."); DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.DownloadAndInstallAppAsync(name, new Uri(url)); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS application '" + name + "' was installed successfully from: " + url); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("installedApp"); WriteAppAsJson(jsonWriter, application); } public async Task DownloadAndUpdateAppAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); string url = request.GetQueryOrForm("url"); if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Parameter 'url' value must start with 'https://'."); DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.DownloadAndUpdateAppAsync(name, new Uri(url)); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS application '" + name + "' was updated successfully from: " + url); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedApp"); WriteAppAsJson(jsonWriter, application); } public async Task InstallAppAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!request.HasFormContentType || (request.Form.Files.Count == 0)) throw new DnsWebServiceException("DNS application zip file is missing."); string tmpFile = Path.GetTempFileName(); try { await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //write to temp file await request.Form.Files[0].CopyToAsync(fS); //install app fS.Position = 0; DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.InstallApplicationAsync(name, fS); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS application '" + name + "' was installed successfully."); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("installedApp"); WriteAppAsJson(jsonWriter, application); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } public async Task UpdateAppAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!request.HasFormContentType || (request.Form.Files.Count == 0)) throw new DnsWebServiceException("DNS application zip file is missing."); string tmpFile = Path.GetTempFileName(); try { await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //write to temp file await request.Form.Files[0].CopyToAsync(fS); //update app fS.Position = 0; DnsApplication application = await _dnsWebService._dnsServer.DnsApplicationManager.UpdateApplicationAsync(name, fS); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS application '" + name + "' was updated successfully."); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedApp"); WriteAppAsJson(jsonWriter, application); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } public void UninstallApp(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); _dnsWebService._dnsServer.DnsApplicationManager.UninstallApplication(name); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS application '" + name + "' was uninstalled successfully."); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public async Task GetAppConfigAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); string config = await application.GetConfigAsync(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("config", config); } public async Task SetAppConfigAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Apps, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name").Trim(); if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); string config = request.QueryOrForm("config"); if (config is null) throw new DnsWebServiceException("Parameter 'config' missing."); if (config.Length == 0) config = null; await application.SetConfigAsync(config); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS application '" + name + "' app config was saved successfully."); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceAuthApi.cs ================================================ /* 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 Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Security.OTP; namespace DnsServerCore { public partial class DnsWebService { sealed class WebServiceAuthApi { #region variables readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceAuthApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region private private void WriteCurrentSessionDetails(Utf8JsonWriter jsonWriter, UserSession currentSession, bool includeInfo) { if (currentSession.Type == UserSessionType.ApiToken) { jsonWriter.WriteString("username", currentSession.User.Username); jsonWriter.WriteString("tokenName", currentSession.TokenName); jsonWriter.WriteString("token", currentSession.Token); } else { jsonWriter.WriteString("displayName", currentSession.User.DisplayName); jsonWriter.WriteString("username", currentSession.User.Username); jsonWriter.WriteBoolean("totpEnabled", currentSession.User.TOTPEnabled); jsonWriter.WriteString("token", currentSession.Token); } if (includeInfo) { jsonWriter.WriteStartObject("info"); jsonWriter.WriteString("version", _dnsWebService.GetServerVersion()); jsonWriter.WriteString("uptimestamp", _dnsWebService._uptimestamp); jsonWriter.WriteString("dnsServerDomain", _dnsWebService._dnsServer.ServerDomain); jsonWriter.WriteNumber("defaultRecordTtl", _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl); jsonWriter.WriteNumber("defaultNsRecordTtl", _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl); jsonWriter.WriteNumber("defaultSoaRecordTtl", _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl); jsonWriter.WriteBoolean("useSoaSerialDateScheme", _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme); jsonWriter.WriteBoolean("dnssecValidation", _dnsWebService._dnsServer.DnssecValidation); jsonWriter.WriteBoolean("clusterInitialized", _dnsWebService._clusterManager.ClusterInitialized); if (_dnsWebService._clusterManager.ClusterInitialized) { jsonWriter.WriteString("clusterDomain", _dnsWebService._clusterManager.ClusterDomain); _dnsWebService._clusterApi.WriteClusterNodes(jsonWriter); } jsonWriter.WriteStartObject("permissions"); for (int i = 1; i <= 11; i++) { PermissionSection section = (PermissionSection)i; jsonWriter.WritePropertyName(section.ToString()); jsonWriter.WriteStartObject(); jsonWriter.WriteBoolean("canView", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.View)); jsonWriter.WriteBoolean("canModify", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.Modify)); jsonWriter.WriteBoolean("canDelete", _dnsWebService._authManager.IsPermitted(section, currentSession.User, PermissionFlag.Delete)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndObject(); jsonWriter.WriteEndObject(); } } private void WriteUserDetails(Utf8JsonWriter jsonWriter, User user, UserSession currentSession, bool includeMoreDetails, bool includeGroups) { jsonWriter.WriteString("displayName", user.DisplayName); jsonWriter.WriteString("username", user.Username); jsonWriter.WriteBoolean("totpEnabled", user.TOTPEnabled); jsonWriter.WriteBoolean("disabled", user.Disabled); jsonWriter.WriteString("previousSessionLoggedOn", user.PreviousSessionLoggedOn); jsonWriter.WriteString("previousSessionRemoteAddress", user.PreviousSessionRemoteAddress.ToString()); jsonWriter.WriteString("recentSessionLoggedOn", user.RecentSessionLoggedOn); jsonWriter.WriteString("recentSessionRemoteAddress", user.RecentSessionRemoteAddress.ToString()); if (includeMoreDetails) { jsonWriter.WriteNumber("sessionTimeoutSeconds", user.SessionTimeoutSeconds); jsonWriter.WritePropertyName("memberOfGroups"); jsonWriter.WriteStartArray(); List memberOfGroups = new List(user.MemberOfGroups); memberOfGroups.Sort(); foreach (Group group in memberOfGroups) { if (group.Name.Equals("Everyone", StringComparison.OrdinalIgnoreCase)) continue; jsonWriter.WriteStringValue(group.Name); } jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("sessions"); jsonWriter.WriteStartArray(); List sessions = _dnsWebService._authManager.GetSessions(user); sessions.Sort(); foreach (UserSession session in sessions) WriteUserSessionDetails(jsonWriter, session, currentSession); jsonWriter.WriteEndArray(); } if (includeGroups) { List groups = new List(_dnsWebService._authManager.Groups); groups.Sort(); jsonWriter.WritePropertyName("groups"); jsonWriter.WriteStartArray(); foreach (Group group in groups) { if (group.Name.Equals("Everyone", StringComparison.OrdinalIgnoreCase)) continue; jsonWriter.WriteStringValue(group.Name); } jsonWriter.WriteEndArray(); } } private static void WriteUserSessionDetails(Utf8JsonWriter jsonWriter, UserSession session, UserSession currentSession) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("username", session.User.Username); jsonWriter.WriteBoolean("isCurrentSession", session.Equals(currentSession)); jsonWriter.WriteString("partialToken", session.Token.AsSpan(0, 16)); jsonWriter.WriteString("type", session.Type.ToString()); jsonWriter.WriteString("tokenName", session.TokenName); jsonWriter.WriteString("lastSeen", session.LastSeen); jsonWriter.WriteString("lastSeenRemoteAddress", session.LastSeenRemoteAddress.ToString()); jsonWriter.WriteString("lastSeenUserAgent", session.LastSeenUserAgent); jsonWriter.WriteEndObject(); } private void WriteGroupDetails(Utf8JsonWriter jsonWriter, Group group, bool includeMembers, bool includeUsers) { jsonWriter.WriteString("name", group.Name); jsonWriter.WriteString("description", group.Description); if (includeMembers) { jsonWriter.WritePropertyName("members"); jsonWriter.WriteStartArray(); List members = _dnsWebService._authManager.GetGroupMembers(group); members.Sort(); foreach (User user in members) jsonWriter.WriteStringValue(user.Username); jsonWriter.WriteEndArray(); } if (includeUsers) { List users = new List(_dnsWebService._authManager.Users); users.Sort(); jsonWriter.WritePropertyName("users"); jsonWriter.WriteStartArray(); foreach (User user in users) jsonWriter.WriteStringValue(user.Username); jsonWriter.WriteEndArray(); } } private void WritePermissionDetails(Utf8JsonWriter jsonWriter, Permission permission, string subItem, bool includeUsersAndGroups) { jsonWriter.WriteString("section", permission.Section.ToString()); if (subItem is not null) jsonWriter.WriteString("subItem", subItem.Length == 0 ? "." : subItem); jsonWriter.WritePropertyName("userPermissions"); jsonWriter.WriteStartArray(); List> userPermissions = new List>(permission.UserPermissions); userPermissions.Sort(delegate (KeyValuePair x, KeyValuePair y) { return x.Key.Username.CompareTo(y.Key.Username); }); foreach (KeyValuePair userPermission in userPermissions) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("username", userPermission.Key.Username); jsonWriter.WriteBoolean("canView", userPermission.Value.HasFlag(PermissionFlag.View)); jsonWriter.WriteBoolean("canModify", userPermission.Value.HasFlag(PermissionFlag.Modify)); jsonWriter.WriteBoolean("canDelete", userPermission.Value.HasFlag(PermissionFlag.Delete)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("groupPermissions"); jsonWriter.WriteStartArray(); List> groupPermissions = new List>(permission.GroupPermissions); groupPermissions.Sort(delegate (KeyValuePair x, KeyValuePair y) { return x.Key.Name.CompareTo(y.Key.Name); }); foreach (KeyValuePair groupPermission in groupPermissions) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", groupPermission.Key.Name); jsonWriter.WriteBoolean("canView", groupPermission.Value.HasFlag(PermissionFlag.View)); jsonWriter.WriteBoolean("canModify", groupPermission.Value.HasFlag(PermissionFlag.Modify)); jsonWriter.WriteBoolean("canDelete", groupPermission.Value.HasFlag(PermissionFlag.Delete)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); if (includeUsersAndGroups) { List users = new List(_dnsWebService._authManager.Users); users.Sort(); List groups = new List(_dnsWebService._authManager.Groups); groups.Sort(); jsonWriter.WritePropertyName("users"); jsonWriter.WriteStartArray(); foreach (User user in users) jsonWriter.WriteStringValue(user.Username); jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("groups"); jsonWriter.WriteStartArray(); foreach (Group group in groups) jsonWriter.WriteStringValue(group.Name); jsonWriter.WriteEndArray(); } } #endregion #region public public async Task LoginAsync(HttpContext context, UserSessionType sessionType) { HttpRequest request = context.Request; string username = request.GetQueryOrForm("user"); string password = request.GetQueryOrForm("pass"); string totp = request.GetQueryOrForm("totp", null); string tokenName = (sessionType == UserSessionType.ApiToken) ? request.GetQueryOrForm("tokenName") : null; bool includeInfo = request.GetQueryOrForm("includeInfo", bool.Parse, false); IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader); UserSession session = await _dnsWebService._authManager.CreateSessionAsync(sessionType, tokenName, username, password, totp, remoteEP.Address, request.Headers.UserAgent); _dnsWebService._log.Write(remoteEP, "[" + session.User.Username + "] User logged in."); _dnsWebService._authManager.SaveConfigFile(); if (sessionType == UserSessionType.ApiToken) { //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteCurrentSessionDetails(jsonWriter, session, includeInfo); } public void Logout(HttpContext context) { string token = context.Request.GetQueryOrForm("token"); UserSession session = _dnsWebService._authManager.DeleteSession(token); if (session is not null) { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + session.User.Username + "] User logged out."); _dnsWebService._authManager.SaveConfigFile(); } } public void GetCurrentSessionDetails(HttpContext context) { UserSession session = context.GetCurrentSession(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteCurrentSessionDetails(jsonWriter, session, true); } public async Task ChangePasswordAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context, true); HttpRequest request = context.Request; string password = request.GetQueryOrForm("pass"); string totp = request.GetQueryOrForm("totp", null); IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader); string newPassword = request.GetQueryOrForm("newPass"); int iterations = request.GetQueryOrForm("iterations", int.Parse, User.DEFAULT_ITERATIONS); sessionUser = await _dnsWebService._authManager.ChangePasswordAsync(sessionUser.Username, password, totp, remoteEP.Address, newPassword, iterations); _dnsWebService._log.Write(remoteEP, "[" + sessionUser.Username + "] Password was changed successfully."); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void Initialize2FA(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context, true); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); if (sessionUser.TOTPEnabled) { jsonWriter.WriteBoolean("totpEnabled", true); } else { AuthenticatorKeyUri totpKeyUri = sessionUser.InitializedTOTP(_dnsWebService._dnsServer.ServerDomain); jsonWriter.WriteBoolean("totpEnabled", false); jsonWriter.WriteString("qrCodePngImage", Convert.ToBase64String(totpKeyUri.GetQRCodePngImage(3))); jsonWriter.WriteString("secret", totpKeyUri.Secret); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Two-factor Authentication (2FA) using Time-based one-time password (TOTP) was initialized successfully."); _dnsWebService._authManager.SaveConfigFile(); } } public void Enable2FA(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context, true); HttpRequest request = context.Request; string totp = request.GetQueryOrForm("totp"); sessionUser.EnableTOTP(totp); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Two-factor Authentication (2FA) using Time-based one-time password (TOTP) was enabled successfully."); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void Disable2FA(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context, true); sessionUser.DisableTOTP(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Two-factor Authentication (2FA) using Time-based one-time password (TOTP) was disabled successfully."); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void GetProfile(HttpContext context) { UserSession session = context.GetCurrentSession(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, session.User, session, true, false); } public void SetProfile(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context, true); HttpRequest request = context.Request; if (request.TryGetQueryOrForm("displayName", out string displayName)) sessionUser.DisplayName = displayName; if (request.TryGetQueryOrForm("sessionTimeoutSeconds", int.Parse, out int sessionTimeoutSeconds)) sessionUser.SessionTimeoutSeconds = sessionTimeoutSeconds; _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] User profile was updated successfully."); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); UserSession session = context.GetCurrentSession(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, sessionUser, session, true, false); } public void ListSessions(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("sessions"); jsonWriter.WriteStartArray(); List sessions = new List(_dnsWebService._authManager.Sessions); sessions.Sort(); UserSession session = context.GetCurrentSession(); foreach (UserSession activeSession in sessions) { if (!activeSession.HasExpired()) WriteUserSessionDetails(jsonWriter, activeSession, session); } jsonWriter.WriteEndArray(); } public void CreateApiToken(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string username = request.GetQueryOrForm("user"); string tokenName = request.GetQueryOrForm("tokenName"); IPEndPoint remoteEP = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader); UserSession createdSession = _dnsWebService._authManager.CreateApiToken(tokenName, username, remoteEP.Address, request.Headers.UserAgent); _dnsWebService._log.Write(remoteEP, "[" + sessionUser.Username + "] API token [" + tokenName + "] was created successfully for user: " + username); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("username", createdSession.User.Username); jsonWriter.WriteString("tokenName", createdSession.TokenName); jsonWriter.WriteString("token", createdSession.Token); } public void DeleteSession(HttpContext context, bool isAdminContext) { UserSession session = context.GetCurrentSession(); if (isAdminContext) { if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, session.User, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); } string strPartialToken = context.Request.GetQueryOrForm("partialToken"); if (session.Token.StartsWith(strPartialToken)) throw new InvalidOperationException("Invalid operation: cannot delete current session."); UserSession sessionToDelete = null; foreach (UserSession activeSession in _dnsWebService._authManager.Sessions) { if (activeSession.Token.StartsWith(strPartialToken)) { sessionToDelete = activeSession; break; } } if (sessionToDelete is null) throw new DnsWebServiceException("No such active session was found for partial token: " + strPartialToken); if (!isAdminContext) { if (sessionToDelete.User != session.User) throw new DnsWebServiceException("Access was denied."); } if (_dnsWebService._clusterManager.ClusterInitialized) { if (sessionToDelete.Type == UserSessionType.ApiToken) { if (sessionToDelete.TokenName.Equals(_dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Invalid operation: cannot delete the Cluster API token."); if (_dnsWebService._clusterManager.GetSelfNode().Type != Cluster.ClusterNodeType.Primary) throw new DnsWebServiceException("API tokens can be deleted only on the Primary node."); } } UserSession deletedSession = _dnsWebService._authManager.DeleteSession(sessionToDelete.Token); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + session.User.Username + "] User session [" + strPartialToken + "] was deleted successfully for user: " + deletedSession.User.Username); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void ListUsers(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); List users = new List(_dnsWebService._authManager.Users); users.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("users"); jsonWriter.WriteStartArray(); foreach (User user in users) { jsonWriter.WriteStartObject(); WriteUserDetails(jsonWriter, user, null, false, false); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public void CreateUser(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string username = request.GetQueryOrForm("user"); string displayName = request.GetQueryOrForm("displayName", username); string password = request.GetQueryOrForm("pass"); User user = _dnsWebService._authManager.CreateUser(displayName, username, password); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] User account was created successfully with username: " + user.Username); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, user, null, false, false); } public void GetUserDetails(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string username = request.GetQueryOrForm("user"); bool includeGroups = request.GetQueryOrForm("includeGroups", bool.Parse, false); User user = _dnsWebService._authManager.GetUser(username); if (user is null) throw new DnsWebServiceException("No such user exists: " + username); UserSession session = context.GetCurrentSession(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, user, session, true, includeGroups); } public void SetUserDetails(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string username = request.GetQueryOrForm("user"); User user = _dnsWebService._authManager.GetUser(username); if (user is null) throw new DnsWebServiceException("No such user exists: " + username); try { if (request.TryGetQueryOrForm("displayName", out string displayName)) user.DisplayName = displayName; if (request.TryGetQueryOrForm("newUser", out string newUsername)) _dnsWebService._authManager.ChangeUsername(user, newUsername); if (request.TryGetQueryOrForm("totpEnabled", bool.Parse, out bool totpEnabled)) { if (totpEnabled) throw new InvalidOperationException("Time-based one-time password (TOTP) can be enabled only by the user themself."); user.DisableTOTP(); } if (request.TryGetQueryOrForm("disabled", bool.Parse, out bool disabled) && (sessionUser != user)) //to avoid self lockout { user.Disabled = disabled; if (user.Disabled) { foreach (UserSession userSession in _dnsWebService._authManager.Sessions) { if (userSession.Type == UserSessionType.ApiToken) continue; if (userSession.User == user) _dnsWebService._authManager.DeleteSession(userSession.Token); } } } if (request.TryGetQueryOrForm("sessionTimeoutSeconds", int.Parse, out int sessionTimeoutSeconds)) user.SessionTimeoutSeconds = sessionTimeoutSeconds; string newPassword = request.QueryOrForm("newPass"); if (!string.IsNullOrWhiteSpace(newPassword)) { int iterations = request.GetQueryOrForm("iterations", int.Parse, User.DEFAULT_ITERATIONS); user.ChangePassword(newPassword, iterations); } string memberOfGroups = request.QueryOrForm("memberOfGroups"); if (memberOfGroups is not null) { string[] parts = memberOfGroups.Split(','); Dictionary groups = new Dictionary(parts.Length); foreach (string part in parts) { if (part.Length == 0) continue; Group group = _dnsWebService._authManager.GetGroup(part); if (group is null) throw new DnsWebServiceException("No such group exists: " + part); groups.Add(group.Name.ToLowerInvariant(), group); } //ensure user is member of everyone group Group everyone = _dnsWebService._authManager.GetGroup(Group.EVERYONE); groups[everyone.Name.ToLowerInvariant()] = everyone; if (sessionUser == user) { //ensure current admin user is member of administrators group to avoid self lockout Group admins = _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS); groups[admins.Name.ToLowerInvariant()] = admins; } user.SyncGroups(groups); } } finally { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] User account details were updated successfully for user: " + username); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } UserSession session = context.GetCurrentSession(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteUserDetails(jsonWriter, user, session, true, false); } public void DeleteUser(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string username = context.Request.GetQueryOrForm("user"); if (sessionUser.Username.Equals(username, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Invalid operation: cannot delete current user."); if (_dnsWebService._clusterManager.ClusterInitialized) { User userToDelete = _dnsWebService._authManager.GetUser(username); if (userToDelete is null) throw new DnsWebServiceException("No such user exists: " + username); List userSessions = _dnsWebService.AuthManager.GetSessions(userToDelete); bool apiTokenExists = false; foreach (UserSession existingSession in userSessions) { if ((existingSession.Type == UserSessionType.ApiToken) && (existingSession.TokenName == _dnsWebService._clusterManager.ClusterDomain)) { apiTokenExists = true; break; } } if (apiTokenExists) throw new DnsWebServiceException("Invalid operation: cannot delete a user who initialized the Cluster and owns the Cluster API token."); } if (!_dnsWebService._authManager.DeleteUser(username)) throw new DnsWebServiceException("Failed to delete user: " + username); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] User account was deleted successfully with username: " + username); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void ListGroups(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); List groups = new List(_dnsWebService._authManager.Groups); groups.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("groups"); jsonWriter.WriteStartArray(); foreach (Group group in groups) { if (group.Name.Equals("Everyone", StringComparison.OrdinalIgnoreCase)) continue; jsonWriter.WriteStartObject(); WriteGroupDetails(jsonWriter, group, false, false); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public void CreateGroup(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string groupName = request.GetQueryOrForm("group"); string description = request.GetQueryOrForm("description", ""); Group group = _dnsWebService._authManager.CreateGroup(groupName, description); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Group was created successfully with name: " + group.Name); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteGroupDetails(jsonWriter, group, false, false); } public void GetGroupDetails(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string groupName = request.GetQueryOrForm("group"); bool includeUsers = request.GetQueryOrForm("includeUsers", bool.Parse, false); Group group = _dnsWebService._authManager.GetGroup(groupName); if (group is null) throw new DnsWebServiceException("No such group exists: " + groupName); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteGroupDetails(jsonWriter, group, true, includeUsers); } public void SetGroupDetails(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string groupName = request.GetQueryOrForm("group"); Group group = _dnsWebService._authManager.GetGroup(groupName); if (group is null) throw new DnsWebServiceException("No such group exists: " + groupName); if (request.TryGetQueryOrForm("newGroup", out string newGroup)) _dnsWebService._authManager.RenameGroup(group, newGroup); if (request.TryGetQueryOrForm("description", out string description)) group.Description = description; string members = request.QueryOrForm("members"); if (members is not null) { string[] parts = members.Split(','); Dictionary users = new Dictionary(); foreach (string part in parts) { if (part.Length == 0) continue; User user = _dnsWebService._authManager.GetUser(part); if (user is null) throw new DnsWebServiceException("No such user exists: " + part); users.Add(user.Username, user); } if (group.Name.Equals("administrators", StringComparison.OrdinalIgnoreCase)) users[sessionUser.Username] = sessionUser; //ensure current admin user is member of administrators group to avoid self lockout _dnsWebService._authManager.SyncGroupMembers(group, users); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Group details were updated successfully for group: " + groupName); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteGroupDetails(jsonWriter, group, true, false); } public void DeleteGroup(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string groupName = context.Request.GetQueryOrForm("group"); if (!_dnsWebService._authManager.DeleteGroup(groupName)) throw new DnsWebServiceException("Failed to delete group: " + groupName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Group was deleted successfully with name: " + groupName); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void ListPermissions(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); List permissions = new List(_dnsWebService._authManager.Permissions); permissions.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("permissions"); jsonWriter.WriteStartArray(); foreach (Permission permission in permissions) { jsonWriter.WriteStartObject(); WritePermissionDetails(jsonWriter, permission, null, false); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public void GetPermissionDetails(HttpContext context, PermissionSection section) { User sessionUser = _dnsWebService.GetSessionUser(context); HttpRequest request = context.Request; string strSubItem = null; switch (section) { case PermissionSection.Unknown: if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); section = request.GetQueryOrFormEnum("section"); break; case PermissionSection.Zones: if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); strSubItem = request.GetQueryOrForm("zone").Trim('.'); break; default: throw new InvalidOperationException(); } bool includeUsersAndGroups = request.GetQueryOrForm("includeUsersAndGroups", bool.Parse, false); if (strSubItem is not null) { if (!_dnsWebService._authManager.IsPermitted(section, strSubItem, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); } Permission permission; if (strSubItem is null) permission = _dnsWebService._authManager.GetPermission(section); else permission = _dnsWebService._authManager.GetPermission(section, strSubItem); if (permission is null) throw new DnsWebServiceException("No permissions exists for section: " + section.ToString() + (strSubItem is null ? "" : "/" + strSubItem)); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WritePermissionDetails(jsonWriter, permission, strSubItem, includeUsersAndGroups); } public void SetPermissionsDetails(HttpContext context, PermissionSection section) { User sessionUser = _dnsWebService.GetSessionUser(context); HttpRequest request = context.Request; string strSubItem = null; switch (section) { case PermissionSection.Unknown: if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); if (_dnsWebService._clusterManager.ClusterInitialized) { if (_dnsWebService._clusterManager.GetSelfNode().Type != Cluster.ClusterNodeType.Primary) throw new DnsWebServiceException("Permissions for sections can be set only on the Primary node."); } section = request.GetQueryOrFormEnum("section"); break; case PermissionSection.Zones: if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); strSubItem = request.GetQueryOrForm("zone").Trim('.'); break; default: throw new InvalidOperationException(); } if (strSubItem is not null) { if (!_dnsWebService._authManager.IsPermitted(section, strSubItem, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); } Permission permission; if (strSubItem is null) permission = _dnsWebService._authManager.GetPermission(section); else permission = _dnsWebService._authManager.GetPermission(section, strSubItem); if (permission is null) throw new DnsWebServiceException("No permissions exists for section: " + section.ToString() + (strSubItem is null ? "" : "/" + strSubItem)); string strUserPermissions = request.QueryOrForm("userPermissions"); if (strUserPermissions is not null) { string[] parts = strUserPermissions.Split('|'); Dictionary userPermissions = new Dictionary(); for (int i = 0; i < parts.Length; i += 4) { if (parts[i].Length == 0) continue; User user = _dnsWebService._authManager.GetUser(parts[i]); bool canView = bool.Parse(parts[i + 1]); bool canModify = bool.Parse(parts[i + 2]); bool canDelete = bool.Parse(parts[i + 3]); if (user is not null) { PermissionFlag permissionFlag = PermissionFlag.None; if (canView) permissionFlag |= PermissionFlag.View; if (canModify) permissionFlag |= PermissionFlag.Modify; if (canDelete) permissionFlag |= PermissionFlag.Delete; userPermissions[user] = permissionFlag; } } permission.SyncPermissions(userPermissions); } string strGroupPermissions = request.QueryOrForm("groupPermissions"); if (strGroupPermissions is not null) { string[] parts = strGroupPermissions.Split('|'); Dictionary groupPermissions = new Dictionary(); for (int i = 0; i < parts.Length; i += 4) { if (parts[i].Length == 0) continue; Group group = _dnsWebService._authManager.GetGroup(parts[i]); bool canView = bool.Parse(parts[i + 1]); bool canModify = bool.Parse(parts[i + 2]); bool canDelete = bool.Parse(parts[i + 3]); if (group is not null) { PermissionFlag permissionFlag = PermissionFlag.None; if (canView) permissionFlag |= PermissionFlag.View; if (canModify) permissionFlag |= PermissionFlag.Modify; if (canDelete) permissionFlag |= PermissionFlag.Delete; groupPermissions[group] = permissionFlag; } } //ensure administrators group always has all permissions Group admins = _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS); groupPermissions[admins] = PermissionFlag.ViewModifyDelete; switch (section) { case PermissionSection.Zones: //ensure DNS administrators group always has all permissions Group dnsAdmins = _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS); groupPermissions[dnsAdmins] = PermissionFlag.ViewModifyDelete; break; case PermissionSection.DhcpServer: //ensure DHCP administrators group always has all permissions Group dhcpAdmins = _dnsWebService._authManager.GetGroup(Group.DHCP_ADMINISTRATORS); groupPermissions[dhcpAdmins] = PermissionFlag.ViewModifyDelete; break; } permission.SyncPermissions(groupPermissions); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Permissions were updated successfully for section: " + section.ToString() + (string.IsNullOrEmpty(strSubItem) ? "" : "/" + strSubItem)); _dnsWebService._authManager.SaveConfigFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WritePermissionDetails(jsonWriter, permission, strSubItem, false); } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceClusterApi.cs ================================================ /* 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 Microsoft.AspNetCore.Http; using System; using System.Buffers.Text; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace DnsServerCore { public partial class DnsWebService { sealed class WebServiceClusterApi { #region variables readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceClusterApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region private private void WriteClusterState(Utf8JsonWriter jsonWriter, bool includeServerIpAddresses = false) { jsonWriter.WriteString("version", _dnsWebService.GetServerVersion()); jsonWriter.WriteString("dnsServerDomain", _dnsWebService._dnsServer.ServerDomain); jsonWriter.WriteBoolean("clusterInitialized", _dnsWebService._clusterManager.ClusterInitialized); if (_dnsWebService._clusterManager.ClusterInitialized) { jsonWriter.WriteString("clusterDomain", _dnsWebService._clusterManager.ClusterDomain); jsonWriter.WriteNumber("heartbeatRefreshIntervalSeconds", _dnsWebService._clusterManager.HeartbeatRefreshIntervalSeconds); jsonWriter.WriteNumber("heartbeatRetryIntervalSeconds", _dnsWebService._clusterManager.HeartBeatRetryIntervalSeconds); jsonWriter.WriteNumber("configRefreshIntervalSeconds", _dnsWebService._clusterManager.ConfigRefreshIntervalSeconds); jsonWriter.WriteNumber("configRetryIntervalSeconds", _dnsWebService._clusterManager.ConfigRetryIntervalSeconds); WriteClusterNodes(jsonWriter); } if (includeServerIpAddresses) { jsonWriter.WriteStartArray("serverIpAddresses"); foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) { if (networkInterface.OperationalStatus != OperationalStatus.Up) continue; foreach (UnicastIPAddressInformation ip in networkInterface.GetIPProperties().UnicastAddresses) { if (IPAddress.IsLoopback(ip.Address)) continue; switch (ip.Address.AddressFamily) { case AddressFamily.InterNetwork: jsonWriter.WriteStringValue(ip.Address.ToString()); break; case AddressFamily.InterNetworkV6: if (ip.Address.IsIPv6LinkLocal || ip.Address.IsIPv6Teredo) continue; jsonWriter.WriteStringValue(ip.Address.ToString()); break; } } } jsonWriter.WriteEndArray(); } } internal void WriteClusterNodes(Utf8JsonWriter jsonWriter) { List sortedClusterNodes = [.. _dnsWebService._clusterManager.ClusterNodes.Values]; sortedClusterNodes.Sort(); jsonWriter.WriteStartArray("clusterNodes"); foreach (ClusterNode clusterNode in sortedClusterNodes) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("id", clusterNode.Id); jsonWriter.WriteString("name", clusterNode.Name); jsonWriter.WriteString("url", clusterNode.Url.OriginalString); jsonWriter.WriteStartArray("ipAddresses"); foreach (IPAddress ipAddress in clusterNode.IPAddresses) jsonWriter.WriteStringValue(ipAddress.ToString()); jsonWriter.WriteEndArray(); jsonWriter.WriteString("type", clusterNode.Type.ToString()); jsonWriter.WriteString("state", clusterNode.State.ToString()); if (clusterNode.State == ClusterNodeState.Self) { jsonWriter.WriteString("upSince", clusterNode.UpSince); if (clusterNode.Type == ClusterNodeType.Secondary) { if (_dnsWebService._clusterManager.ConfigLastSynced != default) jsonWriter.WriteString("configLastSynced", _dnsWebService._clusterManager.ConfigLastSynced); } } else { if (clusterNode.UpSince != default) jsonWriter.WriteString("upSince", clusterNode.UpSince); if (clusterNode.LastSeen != default) jsonWriter.WriteString("lastSeen", clusterNode.LastSeen); } jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } private void EnableWebServiceTlsWithSelfSignedCertificate() { _dnsWebService._webServiceEnableTls = true; _dnsWebService._webServiceUseSelfSignedTlsCertificate = true; _dnsWebService._webServiceTlsCertificatePath = null; _dnsWebService._webServiceTlsCertificatePassword = null; _dnsWebService.CheckAndLoadSelfSignedCertificate(false, true); _dnsWebService.SaveConfigFile(); } private void RestartWebService() { ThreadPool.QueueUserWorkItem(async delegate (object state) { try { await Task.Delay(2000); //wait for the current HTTP response to be delivered before restarting web server _dnsWebService._log.Write("Attempting to restart web service."); await _dnsWebService.StopWebServiceAsync(); await _dnsWebService.StartWebServiceAsync(false); _dnsWebService._log.Write("Web service was restarted successfully."); } catch (Exception ex) { _dnsWebService._log.Write("Failed to restart web service.\r\n" + ex.ToString()); _dnsWebService._log.Write("Attempting to restart web service in HTTP only mode."); try { await _dnsWebService.StopWebServiceAsync(); await _dnsWebService.StartWebServiceAsync(true); } catch (Exception ex2) { _dnsWebService._log.Write("Failed to restart web service in HTTP only mode.\r\n" + ex2.ToString()); } } }); } #endregion #region public public void GetClusterState(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; bool includeServerIpAddresses = request.GetQueryOrForm("includeServerIpAddresses", bool.Parse, false); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter, includeServerIpAddresses); } public void InitializeCluster(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string clusterDomain = request.GetQueryOrForm("clusterDomain").TrimEnd('.'); if (!request.TryGetQueryOrFormArray("primaryNodeIpAddresses", IPAddress.Parse, out IPAddress[] primaryNodeIpAddresses)) throw new DnsWebServiceException("Parameter 'primaryNodeIpAddresses' missing."); bool restartWebService = false; //enable TLS web service if not already enabled if (!_dnsWebService.IsWebServiceTlsEnabled) { EnableWebServiceTlsWithSelfSignedCertificate(); restartWebService = true; } try { _dnsWebService._clusterManager.InitializeCluster(clusterDomain, primaryNodeIpAddresses, context.GetCurrentSession()); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Cluster (" + _dnsWebService._clusterManager.ClusterDomain + ") was initialized successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } finally { //restart TLS web service to apply HTTPS changes if (restartWebService) RestartWebService(); } } public void DeleteCluster(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; bool forceDelete = request.GetQueryOrForm("forceDelete", bool.Parse, false); string clusterDomain = _dnsWebService._clusterManager.ClusterDomain; _dnsWebService._clusterManager.DeleteCluster(forceDelete); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Cluster (" + clusterDomain + ") was deleted successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public void JoinCluster(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; int secondaryNodeId = request.GetQueryOrForm("secondaryNodeId", int.Parse); Uri secondaryNodeUrl = new Uri(request.GetQueryOrForm("secondaryNodeUrl")); if (!request.TryGetQueryOrFormArray("secondaryNodeIpAddresses", IPAddress.Parse, out IPAddress[] secondaryNodeIpAddresses)) throw new DnsWebServiceException("Parameter 'secondaryNodeIpAddresses' missing."); X509Certificate2 secondaryNodeCertificate = X509CertificateLoader.LoadCertificate(Base64Url.DecodeFromChars(request.GetQueryOrForm("secondaryNodeCertificate"))); ClusterNode secondaryNode = _dnsWebService._clusterManager.JoinCluster(secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, secondaryNodeCertificate); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Secondary node '" + secondaryNode.ToString() + "' joined the Cluster (" + _dnsWebService._clusterManager.ClusterDomain + ") successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public async Task RemoveSecondaryNodeAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; int secondaryNodeId = request.GetQueryOrForm("secondaryNodeId", int.Parse); ClusterNode secondaryNode = await _dnsWebService._clusterManager.AskSecondaryNodeToLeaveClusterAsync(secondaryNodeId); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Secondary node '" + secondaryNode.ToString() + "' was asked to leave the Cluster (" + _dnsWebService._clusterManager.ClusterDomain + ") successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public void DeleteSecondaryNode(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; int secondaryNodeId = request.GetQueryOrForm("secondaryNodeId", int.Parse); ClusterNode secondaryNode = _dnsWebService._clusterManager.DeleteSecondaryNode(secondaryNodeId); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Secondary node '" + secondaryNode.ToString() + "' was deleted from the Cluster (" + _dnsWebService._clusterManager.ClusterDomain + ") successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public void UpdateSecondaryNode(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; int secondaryNodeId = request.GetQueryOrForm("secondaryNodeId", int.Parse); Uri secondaryNodeUrl = new Uri(request.GetQueryOrForm("secondaryNodeUrl")); if (!request.TryGetQueryOrFormArray("secondaryNodeIpAddresses", IPAddress.Parse, out IPAddress[] secondaryNodeIpAddresses)) throw new DnsWebServiceException("Parameter 'secondaryNodeIpAddresses' missing."); X509Certificate2 secondaryNodeCertificate = X509CertificateLoader.LoadCertificate(Base64Url.DecodeFromChars(request.GetQueryOrForm("secondaryNodeCertificate"))); ClusterNode secondaryNode = _dnsWebService._clusterManager.UpdateSecondaryNode(secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, secondaryNodeCertificate); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Secondary node '" + secondaryNode.ToString() + "' details were updated successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public async Task TransferConfigAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string ifModifiedSinceValue = request.Headers.IfModifiedSince; string includeZonesValue = request.QueryOrForm("includeZones"); DateTime ifModifiedSince = string.IsNullOrEmpty(ifModifiedSinceValue) ? DateTime.UnixEpoch : DateTime.ParseExact(ifModifiedSinceValue, "R", CultureInfo.InvariantCulture); string[] includeZones = string.IsNullOrEmpty(includeZonesValue) ? null : includeZonesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); string tmpFile = Path.GetTempFileName(); try { await using (FileStream configZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //create config zip file await _dnsWebService._clusterManager.TransferConfigAsync(configZipStream, ifModifiedSince, includeZones); //send config zip file configZipStream.Position = 0; HttpResponse response = context.Response; response.ContentType = "application/zip"; response.ContentLength = configZipStream.Length; response.Headers.LastModified = DateTime.UtcNow.ToString("R"); response.Headers.Append("Content-Disposition", "attachment; filename=\"config.zip\""); await using (Stream output = response.Body) { await configZipStream.CopyToAsync(output); } } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Server configuration was transferred successfully."); } public void SetClusterOptions(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; ushort heartbeatRefreshIntervalSeconds = request.GetQueryOrForm("heartbeatRefreshIntervalSeconds", ushort.Parse, _dnsWebService._clusterManager.HeartbeatRefreshIntervalSeconds); ushort heartbeatRetryIntervalSeconds = request.GetQueryOrForm("heartbeatRetryIntervalSeconds", ushort.Parse, _dnsWebService._clusterManager.HeartBeatRetryIntervalSeconds); ushort configRefreshIntervalSeconds = request.GetQueryOrForm("configRefreshIntervalSeconds", ushort.Parse, _dnsWebService._clusterManager.ConfigRefreshIntervalSeconds); ushort configRetryIntervalSeconds = request.GetQueryOrForm("configRetryIntervalSeconds", ushort.Parse, _dnsWebService._clusterManager.ConfigRetryIntervalSeconds); _dnsWebService._clusterManager.UpdateClusterOptions(heartbeatRefreshIntervalSeconds, heartbeatRetryIntervalSeconds, configRefreshIntervalSeconds, configRetryIntervalSeconds); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Cluster (" + _dnsWebService._clusterManager.ClusterDomain + ") options were updated successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public async Task InitializeAndJoinClusterAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; if (!request.TryGetQueryOrFormArray("secondaryNodeIpAddresses", IPAddress.Parse, out IPAddress[] secondaryNodeIpAddresses)) throw new DnsWebServiceException("Parameter 'secondaryNodeIpAddresses' missing."); Uri primaryNodeUrl = new Uri(request.GetQueryOrForm("primaryNodeUrl")); IPAddress primaryNodeIpAddress = request.GetQueryOrForm("primaryNodeIpAddress", IPAddress.Parse, null); string primaryNodeUsername = request.GetQueryOrForm("primaryNodeUsername"); string primaryNodePassword = request.GetQueryOrForm("primaryNodePassword"); string primaryNodeTotp = request.GetQueryOrForm("primaryNodeTotp", null); bool ignoreCertificateErrors = request.GetQueryOrForm("ignoreCertificateErrors", bool.Parse, false); bool restartWebService = false; //enable TLS web service if not already enabled if (!_dnsWebService.IsWebServiceTlsEnabled) { EnableWebServiceTlsWithSelfSignedCertificate(); restartWebService = true; } try { await _dnsWebService._clusterManager.InitializeAndJoinClusterAsync(secondaryNodeIpAddresses, primaryNodeUrl, primaryNodeUsername, primaryNodePassword, primaryNodeTotp, primaryNodeIpAddress is null ? null : [primaryNodeIpAddress], ignoreCertificateErrors); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Joined the Cluster (" + _dnsWebService._clusterManager.ClusterDomain + ") as a Secondary node successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } finally { //restart TLS web service to apply HTTPS changes if (restartWebService) RestartWebService(); } } public async Task LeaveClusterAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; bool forceLeave = request.GetQueryOrForm("forceLeave", bool.Parse, false); string clusterDomain = _dnsWebService._clusterManager.ClusterDomain; await _dnsWebService._clusterManager.LeaveClusterAsync(forceLeave); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Left the Cluster (" + clusterDomain + ") successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public async Task ConfigUpdateNotificationAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; int primaryNodeId = request.GetQueryOrForm("primaryNodeId", int.Parse); Uri primaryNodeUrl = new Uri(request.GetQueryOrForm("primaryNodeUrl")); if (!request.TryGetQueryOrFormArray("primaryNodeIpAddresses", IPAddress.Parse, out IPAddress[] primaryNodeIpAddresses)) throw new DnsWebServiceException("Parameter 'primaryNodeIpAddresses' missing."); //update primary node ClusterNode primaryNode = await _dnsWebService._clusterManager.UpdatePrimaryNodeAsync(primaryNodeUrl, primaryNodeIpAddresses, primaryNodeId); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Notification for configuration update was received. Primary node '" + primaryNode.ToString() + "' details were updated successfully."); } public void ResyncCluster(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._clusterManager.TriggerResyncForConfig(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Resync for configuration and Cluster Secondary zones was triggered successfully."); } public async Task UpdatePrimaryNodeAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; Uri primaryNodeUrl = new Uri(request.GetQueryOrForm("primaryNodeUrl")); if (!request.TryGetQueryOrFormArray("primaryNodeIpAddresses", IPAddress.Parse, out IPAddress[] primaryNodeIpAddresses)) primaryNodeIpAddresses = null; //update primary node ClusterNode primaryNode = await _dnsWebService._clusterManager.UpdatePrimaryNodeAsync(primaryNodeUrl, primaryNodeIpAddresses); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary node '" + primaryNode.ToString() + "' details were updated successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public async Task PromoteToPrimaryNodeAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; bool forceDeletePrimary = request.GetQueryOrForm("forceDeletePrimary", bool.Parse, false); //promote to primary node await _dnsWebService._clusterManager.PromoteToPrimaryNodeAsync(forceDeletePrimary); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] This Secondary node was promoted to be a Primary node for the Cluster successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } public void UpdateSelfNodeIPAddress(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Administration, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; if (!request.TryGetQueryOrFormArray("ipAddresses", IPAddress.Parse, out IPAddress[] ipAddresses)) throw new DnsWebServiceException("Parameter 'ipAddresses' missing."); //update self node IP address ClusterNode selfNode = _dnsWebService._clusterManager.UpdateSelfNodeIPAddresses(ipAddresses); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + selfNode.Type.ToString() + " node '" + selfNode.ToString() + "' IP address was updated successfully."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteClusterState(jsonWriter); } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceDashboardApi.cs ================================================ /* 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.HttpApi.Models; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore { public partial class DnsWebService { class WebServiceDashboardApi { #region variables readonly DnsWebService _dnsWebService; const int CLUSTER_NODE_DASHBOARD_STATS_API_TIMEOUT = 10000; #endregion #region constructor public WebServiceDashboardApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region private private static void WriteChartDataSet(Utf8JsonWriter jsonWriter, DashboardStats.DataSet dataSet, string backgroundColor, string borderColor) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("label", dataSet.Label); jsonWriter.WriteString("backgroundColor", backgroundColor); jsonWriter.WriteString("borderColor", borderColor); jsonWriter.WriteNumber("borderWidth", 2); jsonWriter.WriteBoolean("fill", true); jsonWriter.WritePropertyName("data"); jsonWriter.WriteStartArray(); foreach (long value in dataSet.Data) jsonWriter.WriteNumberValue(value); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); } private async Task ResolvePtrTopClientsAsync(DashboardStats.TopClientStats[] topClients) { IDictionary dhcpClientIpMap = _dnsWebService._dhcpServer.GetAddressHostNameMap(); async Task ResolvePtrAsync(DashboardStats.TopClientStats item) { string ip = item.Name; if (dhcpClientIpMap.TryGetValue(ip, out string dhcpDomain)) { item.Domain = dhcpDomain; return; } IPAddress address = IPAddress.Parse(ip); if (IPAddress.IsLoopback(address)) { item.Domain = "localhost"; return; } DnsDatagram ptrResponse = await _dnsWebService._dnsServer.DirectQueryAsync(new DnsQuestionRecord(address, DnsClass.IN), 500); if (ptrResponse.Answer.Count > 0) { IReadOnlyList ptrDomains = DnsClient.ParseResponsePTR(ptrResponse); if (ptrDomains.Count > 0) { item.Domain = ptrDomains[0]; return; } } } List resolverTasks = new List(topClients.Length); foreach (DashboardStats.TopClientStats item in topClients) { if (string.IsNullOrEmpty(item.Domain)) resolverTasks.Add(ResolvePtrAsync(item)); } foreach (Task resolverTask in resolverTasks) { try { await resolverTask; } catch { } } } #endregion #region public public async Task GetStats(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; DashboardStatsType type = request.GetQueryOrFormEnum("type", DashboardStatsType.LastHour); bool utcFormat = request.GetQueryOrForm("utc", bool.Parse, false); bool isLanguageEnUs = true; string acceptLanguage = request.Headers.AcceptLanguage; if (!string.IsNullOrEmpty(acceptLanguage)) isLanguageEnUs = acceptLanguage.StartsWith("en-us", StringComparison.OrdinalIgnoreCase); bool dontTrimQueryTypeData = request.GetQueryOrForm("dontTrimQueryTypeData", bool.Parse, false); DateTime startDate = default; DateTime endDate = default; if (type == DashboardStatsType.Custom) { string strStartDate = request.GetQueryOrForm("start"); string strEndDate = request.GetQueryOrForm("end"); if (!DateTime.TryParse(strStartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out startDate)) throw new DnsWebServiceException("Invalid start date format."); if (!DateTime.TryParse(strEndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out endDate)) throw new DnsWebServiceException("Invalid end date format."); if (startDate > endDate) throw new DnsWebServiceException("Start date must be less than or equal to end date."); } List> tasks = null; if (_dnsWebService._clusterManager.ClusterInitialized) { string node = request.GetQueryOrForm("node", null); if ("cluster".Equals(node, StringComparison.OrdinalIgnoreCase)) { IReadOnlyDictionary clusterNodes = _dnsWebService._clusterManager.ClusterNodes; tasks = new List>(clusterNodes.Count); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.State == Cluster.ClusterNodeState.Self) continue; tasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return clusterNode.Value.GetDashboardStatsAsync(sessionUser, type, utcFormat, acceptLanguage, true, startDate, endDate, cancellationToken1); }, CLUSTER_NODE_DASHBOARD_STATS_API_TIMEOUT)); } } } DashboardStats dashboardStats; string labelFormat; switch (type) { case DashboardStatsType.LastHour: dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastHourMinuteWiseStats(utcFormat); labelFormat = "HH:mm"; break; case DashboardStatsType.LastDay: dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastDayHourWiseStats(utcFormat); if (isLanguageEnUs) labelFormat = "MM/DD HH:00"; else labelFormat = "DD/MM HH:00"; break; case DashboardStatsType.LastWeek: dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastWeekDayWiseStats(utcFormat); if (isLanguageEnUs) labelFormat = "MM/DD"; else labelFormat = "DD/MM"; break; case DashboardStatsType.LastMonth: dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastMonthDayWiseStats(utcFormat); if (isLanguageEnUs) labelFormat = "MM/DD"; else labelFormat = "DD/MM"; break; case DashboardStatsType.LastYear: labelFormat = "MM/YYYY"; dashboardStats = _dnsWebService._dnsServer.StatsManager.GetLastYearMonthWiseStats(utcFormat); break; case DashboardStatsType.Custom: TimeSpan duration = endDate - startDate; if ((Convert.ToInt32(duration.TotalDays) + 1) > 7) { dashboardStats = _dnsWebService._dnsServer.StatsManager.GetDayWiseStats(startDate, endDate, utcFormat); if (isLanguageEnUs) labelFormat = "MM/DD"; else labelFormat = "DD/MM"; } else if ((Convert.ToInt32(duration.TotalHours) + 1) > 3) { dashboardStats = _dnsWebService._dnsServer.StatsManager.GetHourWiseStats(startDate, endDate, utcFormat); if (isLanguageEnUs) labelFormat = "MM/DD HH:00"; else labelFormat = "DD/MM HH:00"; } else { dashboardStats = _dnsWebService._dnsServer.StatsManager.GetMinuteWiseStats(startDate, endDate, utcFormat); if (isLanguageEnUs) labelFormat = "MM/DD HH:mm"; else labelFormat = "DD/MM HH:mm"; } break; default: throw new DnsWebServiceException("Unknown stats type requested: " + type.ToString()); } //add extra stats { dashboardStats.Stats.Zones = _dnsWebService._dnsServer.AuthZoneManager.TotalZones; dashboardStats.Stats.CachedEntries = _dnsWebService._dnsServer.CacheZoneManager.TotalEntries; dashboardStats.Stats.AllowedZones = _dnsWebService._dnsServer.AllowedZoneManager.TotalZonesAllowed; dashboardStats.Stats.BlockedZones = _dnsWebService._dnsServer.BlockedZoneManager.TotalZonesBlocked; dashboardStats.Stats.AllowListZones = _dnsWebService._dnsServer.BlockListZoneManager.TotalZonesAllowed; dashboardStats.Stats.BlockListZones = _dnsWebService._dnsServer.BlockListZoneManager.TotalZonesBlocked; } if (tasks is not null) { foreach (Task task in tasks) { try { dashboardStats.Merge(await task, 10); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } if (!dontTrimQueryTypeData) dashboardStats.QueryTypeChartData.Trim(10); //trim query type data Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); //stats { jsonWriter.WritePropertyName("stats"); jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("totalQueries", dashboardStats.Stats.TotalQueries); jsonWriter.WriteNumber("totalNoError", dashboardStats.Stats.TotalNoError); jsonWriter.WriteNumber("totalServerFailure", dashboardStats.Stats.TotalServerFailure); jsonWriter.WriteNumber("totalNxDomain", dashboardStats.Stats.TotalNxDomain); jsonWriter.WriteNumber("totalRefused", dashboardStats.Stats.TotalRefused); jsonWriter.WriteNumber("totalAuthoritative", dashboardStats.Stats.TotalAuthoritative); jsonWriter.WriteNumber("totalRecursive", dashboardStats.Stats.TotalRecursive); jsonWriter.WriteNumber("totalCached", dashboardStats.Stats.TotalCached); jsonWriter.WriteNumber("totalBlocked", dashboardStats.Stats.TotalBlocked); jsonWriter.WriteNumber("totalDropped", dashboardStats.Stats.TotalDropped); jsonWriter.WriteNumber("totalClients", dashboardStats.Stats.TotalClients); jsonWriter.WriteNumber("zones", dashboardStats.Stats.Zones); jsonWriter.WriteNumber("cachedEntries", dashboardStats.Stats.CachedEntries); jsonWriter.WriteNumber("allowedZones", dashboardStats.Stats.AllowedZones); jsonWriter.WriteNumber("blockedZones", dashboardStats.Stats.BlockedZones); jsonWriter.WriteNumber("allowListZones", dashboardStats.Stats.AllowListZones); jsonWriter.WriteNumber("blockListZones", dashboardStats.Stats.BlockListZones); jsonWriter.WriteEndObject(); } //main chart { jsonWriter.WritePropertyName("mainChartData"); jsonWriter.WriteStartObject(); //label format { jsonWriter.WriteString("labelFormat", labelFormat); } //label { jsonWriter.WritePropertyName("labels"); jsonWriter.WriteStartArray(); foreach (string label in dashboardStats.MainChartData.Labels) jsonWriter.WriteStringValue(label); jsonWriter.WriteEndArray(); } //datasets { jsonWriter.WritePropertyName("datasets"); jsonWriter.WriteStartArray(); foreach (DashboardStats.DataSet dataSet in dashboardStats.MainChartData.DataSets) { string backgroundColor; string borderColor; switch (dataSet.Label) { case "Total": backgroundColor = "rgba(102, 153, 255, 0.1)"; borderColor = "rgb(102, 153, 255)"; break; case "No Error": backgroundColor = "rgba(92, 184, 92, 0.1)"; borderColor = "rgb(92, 184, 92)"; break; case "Server Failure": backgroundColor = "rgba(217, 83, 79, 0.1)"; borderColor = "rgb(217, 83, 79)"; break; case "NX Domain": backgroundColor = "rgba(120, 120, 120, 0.1)"; borderColor = "rgb(120, 120, 120)"; break; case "Refused": backgroundColor = "rgba(91, 192, 222, 0.1)"; borderColor = "rgb(91, 192, 222)"; break; case "Authoritative": backgroundColor = "rgba(150, 150, 0, 0.1)"; borderColor = "rgb(150, 150, 0)"; break; case "Recursive": backgroundColor = "rgba(23, 162, 184, 0.1)"; borderColor = "rgb(23, 162, 184)"; break; case "Cached": backgroundColor = "rgba(111, 84, 153, 0.1)"; borderColor = "rgb(111, 84, 153)"; break; case "Blocked": backgroundColor = "rgba(255, 165, 0, 0.1)"; borderColor = "rgb(255, 165, 0)"; break; case "Dropped": backgroundColor = "rgba(30, 30, 30, 0.1)"; borderColor = "rgb(30, 30, 30)"; break; case "Clients": backgroundColor = "rgba(51, 122, 183, 0.1)"; borderColor = "rgb(51, 122, 183)"; break; default: throw new InvalidOperationException(); } WriteChartDataSet(jsonWriter, dataSet, backgroundColor, borderColor); } jsonWriter.WriteEndArray(); } jsonWriter.WriteEndObject(); } //query response chart { jsonWriter.WritePropertyName("queryResponseChartData"); jsonWriter.WriteStartObject(); //labels { jsonWriter.WritePropertyName("labels"); jsonWriter.WriteStartArray(); foreach (string label in dashboardStats.QueryResponseChartData.Labels) jsonWriter.WriteStringValue(label); jsonWriter.WriteEndArray(); } //datasets { jsonWriter.WritePropertyName("datasets"); jsonWriter.WriteStartArray(); jsonWriter.WriteStartObject(); jsonWriter.WritePropertyName("data"); jsonWriter.WriteStartArray(); foreach (long value in dashboardStats.QueryResponseChartData.DataSets[0].Data) jsonWriter.WriteNumberValue(value); jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("backgroundColor"); jsonWriter.WriteStartArray(); jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)"); jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)"); jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)"); jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)"); jsonWriter.WriteStringValue("rgba(7, 7, 7, 0.5)"); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); jsonWriter.WriteEndArray(); } jsonWriter.WriteEndObject(); } //query type chart { jsonWriter.WritePropertyName("queryTypeChartData"); jsonWriter.WriteStartObject(); //labels { jsonWriter.WritePropertyName("labels"); jsonWriter.WriteStartArray(); foreach (string label in dashboardStats.QueryTypeChartData.Labels) jsonWriter.WriteStringValue(label); jsonWriter.WriteEndArray(); } //datasets { jsonWriter.WritePropertyName("datasets"); jsonWriter.WriteStartArray(); jsonWriter.WriteStartObject(); jsonWriter.WritePropertyName("data"); jsonWriter.WriteStartArray(); foreach (long value in dashboardStats.QueryTypeChartData.DataSets[0].Data) jsonWriter.WriteNumberValue(value); jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("backgroundColor"); jsonWriter.WriteStartArray(); jsonWriter.WriteStringValue("rgba(102, 153, 255, 0.5)"); jsonWriter.WriteStringValue("rgba(92, 184, 92, 0.5)"); jsonWriter.WriteStringValue("rgba(7, 7, 7, 0.5)"); jsonWriter.WriteStringValue("rgba(91, 192, 222, 0.5)"); jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)"); jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)"); jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)"); jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)"); jsonWriter.WriteStringValue("rgba(51, 122, 183, 0.5)"); jsonWriter.WriteStringValue("rgba(150, 150, 150, 0.5)"); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); jsonWriter.WriteEndArray(); } jsonWriter.WriteEndObject(); } //protocol type chart { jsonWriter.WritePropertyName("protocolTypeChartData"); jsonWriter.WriteStartObject(); //labels { jsonWriter.WritePropertyName("labels"); jsonWriter.WriteStartArray(); foreach (string label in dashboardStats.ProtocolTypeChartData.Labels) jsonWriter.WriteStringValue(label); jsonWriter.WriteEndArray(); } //datasets { jsonWriter.WritePropertyName("datasets"); jsonWriter.WriteStartArray(); jsonWriter.WriteStartObject(); jsonWriter.WritePropertyName("data"); jsonWriter.WriteStartArray(); foreach (long value in dashboardStats.ProtocolTypeChartData.DataSets[0].Data) jsonWriter.WriteNumberValue(value); jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("backgroundColor"); jsonWriter.WriteStartArray(); jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)"); jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)"); jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)"); ; jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)"); jsonWriter.WriteStringValue("rgba(91, 192, 222, 0.5)"); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); jsonWriter.WriteEndArray(); } jsonWriter.WriteEndObject(); } //top clients { await ResolvePtrTopClientsAsync(dashboardStats.TopClients); jsonWriter.WritePropertyName("topClients"); jsonWriter.WriteStartArray(); foreach (DashboardStats.TopClientStats item in dashboardStats.TopClients) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", item.Name); if (!string.IsNullOrEmpty(item.Domain)) jsonWriter.WriteString("domain", item.Domain); jsonWriter.WriteNumber("hits", item.Hits); IPAddress ip = IPAddress.Parse(item.Name); jsonWriter.WriteBoolean("rateLimited", item.RateLimited || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Udp) || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Tcp)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } //top domains { jsonWriter.WritePropertyName("topDomains"); jsonWriter.WriteStartArray(); foreach (DashboardStats.TopStats item in dashboardStats.TopDomains) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", item.Name); if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn)) jsonWriter.WriteString("nameIdn", idn); jsonWriter.WriteNumber("hits", item.Hits); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } //top blocked domains { jsonWriter.WritePropertyName("topBlockedDomains"); jsonWriter.WriteStartArray(); foreach (DashboardStats.TopStats item in dashboardStats.TopBlockedDomains) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", item.Name); if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn)) jsonWriter.WriteString("nameIdn", idn); jsonWriter.WriteNumber("hits", item.Hits); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } } public async Task GetTopStats(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; DashboardStatsType type = request.GetQueryOrFormEnum("type", DashboardStatsType.LastHour); DashboardTopStatsType statsType = request.GetQueryOrFormEnum("statsType"); int limit = request.GetQueryOrForm("limit", int.Parse, 1000); DateTime startDate = default; DateTime endDate = default; if (type == DashboardStatsType.Custom) { string strStartDate = request.GetQueryOrForm("start"); string strEndDate = request.GetQueryOrForm("end"); if (!DateTime.TryParse(strStartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out startDate)) throw new DnsWebServiceException("Invalid start date format."); if (!DateTime.TryParse(strEndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out endDate)) throw new DnsWebServiceException("Invalid end date format."); if (startDate > endDate) throw new DnsWebServiceException("Start date must be less than or equal to end date."); } List> tasks = null; if (_dnsWebService._clusterManager.ClusterInitialized) { string node = request.GetQueryOrForm("node", null); if ("cluster".Equals(node, StringComparison.OrdinalIgnoreCase)) { IReadOnlyDictionary clusterNodes = _dnsWebService._clusterManager.ClusterNodes; tasks = new List>(clusterNodes.Count); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.State == Cluster.ClusterNodeState.Self) continue; tasks.Add(TechnitiumLibrary.TaskExtensions.TimeoutAsync(delegate (CancellationToken cancellationToken1) { return clusterNode.Value.GetDashboardTopStatsAsync(sessionUser, statsType, limit, type, startDate, endDate, cancellationToken1); }, CLUSTER_NODE_DASHBOARD_STATS_API_TIMEOUT)); } } } DashboardStats topStatsData; switch (type) { case DashboardStatsType.LastHour: topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastHourTopStats(statsType, limit); break; case DashboardStatsType.LastDay: topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastDayTopStats(statsType, limit); break; case DashboardStatsType.LastWeek: topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastWeekTopStats(statsType, limit); break; case DashboardStatsType.LastMonth: topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastMonthTopStats(statsType, limit); break; case DashboardStatsType.LastYear: topStatsData = _dnsWebService._dnsServer.StatsManager.GetLastYearTopStats(statsType, limit); break; case DashboardStatsType.Custom: TimeSpan duration = endDate - startDate; if ((Convert.ToInt32(duration.TotalDays) + 1) > 7) topStatsData = _dnsWebService._dnsServer.StatsManager.GetDayWiseTopStats(startDate, endDate, statsType, limit); else if ((Convert.ToInt32(duration.TotalHours) + 1) > 3) topStatsData = _dnsWebService._dnsServer.StatsManager.GetHourWiseTopStats(startDate, endDate, statsType, limit); else topStatsData = _dnsWebService._dnsServer.StatsManager.GetMinuteWiseTopStats(startDate, endDate, statsType, limit); break; default: throw new DnsWebServiceException("Unknown stats type requested: " + type.ToString()); } if (tasks is not null) { foreach (Task task in tasks) { try { topStatsData.Merge(await task, limit); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); switch (statsType) { case DashboardTopStatsType.TopClients: { bool noReverseLookup = request.GetQueryOrForm("noReverseLookup", bool.Parse, false); bool onlyRateLimitedClients = request.GetQueryOrForm("onlyRateLimitedClients", bool.Parse, false); if (!noReverseLookup) await ResolvePtrTopClientsAsync(topStatsData.TopClients); jsonWriter.WritePropertyName("topClients"); jsonWriter.WriteStartArray(); foreach (DashboardStats.TopClientStats item in topStatsData.TopClients) { IPAddress ip = IPAddress.Parse(item.Name); bool rateLimited = item.RateLimited || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Udp) || _dnsWebService._dnsServer.HasQpmLimitExceeded(ip, DnsTransportProtocol.Tcp); if (onlyRateLimitedClients && !rateLimited) continue; jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", item.Name); if (!string.IsNullOrEmpty(item.Domain)) jsonWriter.WriteString("domain", item.Domain); jsonWriter.WriteNumber("hits", item.Hits); jsonWriter.WriteBoolean("rateLimited", rateLimited); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } break; case DashboardTopStatsType.TopDomains: { jsonWriter.WritePropertyName("topDomains"); jsonWriter.WriteStartArray(); foreach (DashboardStats.TopStats item in topStatsData.TopDomains) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", item.Name); if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn)) jsonWriter.WriteString("nameIdn", idn); jsonWriter.WriteNumber("hits", item.Hits); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } break; case DashboardTopStatsType.TopBlockedDomains: { jsonWriter.WritePropertyName("topBlockedDomains"); jsonWriter.WriteStartArray(); foreach (DashboardStats.TopStats item in topStatsData.TopBlockedDomains) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", item.Name); if (DnsClient.TryConvertDomainNameToUnicode(item.Name, out string idn)) jsonWriter.WriteString("nameIdn", idn); jsonWriter.WriteNumber("hits", item.Hits); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } break; default: throw new NotSupportedException(); } } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceDhcpApi.cs ================================================ /* 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.Dhcp; using DnsServerCore.Dhcp.Options; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore { public partial class DnsWebService { class WebServiceDhcpApi { #region variables static readonly char[] _commaSeparator = new char[] { ',' }; readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceDhcpApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region public public void ListDhcpLeases(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); IReadOnlyDictionary scopes = _dnsWebService._dhcpServer.Scopes; //sort by name List sortedScopes = new List(scopes.Count); foreach (KeyValuePair entry in scopes) sortedScopes.Add(entry.Value); sortedScopes.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("leases"); jsonWriter.WriteStartArray(); foreach (Scope scope in sortedScopes) { IReadOnlyDictionary leases = scope.Leases; //sort by address List sortedLeases = new List(leases.Count); foreach (KeyValuePair entry in leases) sortedLeases.Add(entry.Value); sortedLeases.Sort(); foreach (Lease lease in sortedLeases) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("scope", scope.Name); jsonWriter.WriteString("type", lease.Type.ToString()); jsonWriter.WriteString("hardwareAddress", BitConverter.ToString(lease.HardwareAddress)); jsonWriter.WriteString("clientIdentifier", lease.ClientIdentifier.ToString()); jsonWriter.WriteString("address", lease.Address.ToString()); jsonWriter.WriteString("hostName", lease.HostName); jsonWriter.WriteString("leaseObtained", lease.LeaseObtained); jsonWriter.WriteString("leaseExpires", lease.LeaseExpires); jsonWriter.WriteEndObject(); } } jsonWriter.WriteEndArray(); } public void ListDhcpScopes(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); IReadOnlyDictionary scopes = _dnsWebService._dhcpServer.Scopes; //sort by name List sortedScopes = new List(scopes.Count); foreach (KeyValuePair entry in scopes) sortedScopes.Add(entry.Value); sortedScopes.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("scopes"); jsonWriter.WriteStartArray(); foreach (Scope scope in sortedScopes) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", scope.Name); jsonWriter.WriteBoolean("enabled", scope.Enabled); jsonWriter.WriteString("startingAddress", scope.StartingAddress.ToString()); jsonWriter.WriteString("endingAddress", scope.EndingAddress.ToString()); jsonWriter.WriteString("subnetMask", scope.SubnetMask.ToString()); jsonWriter.WriteString("networkAddress", scope.NetworkAddress.ToString()); jsonWriter.WriteString("broadcastAddress", scope.BroadcastAddress.ToString()); if (scope.InterfaceAddress is not null) jsonWriter.WriteString("interfaceAddress", scope.InterfaceAddress.ToString()); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public void GetDhcpScope(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); string scopeName = context.Request.GetQueryOrForm("name"); Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("DHCP scope was not found: " + scopeName); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("name", scope.Name); jsonWriter.WriteString("startingAddress", scope.StartingAddress.ToString()); jsonWriter.WriteString("endingAddress", scope.EndingAddress.ToString()); jsonWriter.WriteString("subnetMask", scope.SubnetMask.ToString()); jsonWriter.WriteNumber("leaseTimeDays", scope.LeaseTimeDays); jsonWriter.WriteNumber("leaseTimeHours", scope.LeaseTimeHours); jsonWriter.WriteNumber("leaseTimeMinutes", scope.LeaseTimeMinutes); jsonWriter.WriteNumber("offerDelayTime", scope.OfferDelayTime); jsonWriter.WriteBoolean("pingCheckEnabled", scope.PingCheckEnabled); jsonWriter.WriteNumber("pingCheckTimeout", scope.PingCheckTimeout); jsonWriter.WriteNumber("pingCheckRetries", scope.PingCheckRetries); if (!string.IsNullOrEmpty(scope.DomainName)) jsonWriter.WriteString("domainName", scope.DomainName); if (scope.DomainSearchList is not null) { jsonWriter.WritePropertyName("domainSearchList"); jsonWriter.WriteStartArray(); foreach (string domainSearchString in scope.DomainSearchList) jsonWriter.WriteStringValue(domainSearchString); jsonWriter.WriteEndArray(); } jsonWriter.WriteBoolean("dnsUpdates", scope.DnsUpdates); jsonWriter.WriteBoolean("dnsOverwriteForDynamicLease", scope.DnsOverwriteForDynamicLease); jsonWriter.WriteNumber("dnsTtl", scope.DnsTtl); if (scope.ServerAddress is not null) jsonWriter.WriteString("serverAddress", scope.ServerAddress.ToString()); if (scope.ServerHostName is not null) jsonWriter.WriteString("serverHostName", scope.ServerHostName); if (scope.BootFileName is not null) jsonWriter.WriteString("bootFileName", scope.BootFileName); if (scope.RouterAddress is not null) jsonWriter.WriteString("routerAddress", scope.RouterAddress.ToString()); jsonWriter.WriteBoolean("useThisDnsServer", scope.UseThisDnsServer); if (scope.DnsServers is not null) { jsonWriter.WritePropertyName("dnsServers"); jsonWriter.WriteStartArray(); foreach (IPAddress dnsServer in scope.DnsServers) jsonWriter.WriteStringValue(dnsServer.ToString()); jsonWriter.WriteEndArray(); } if (scope.WinsServers is not null) { jsonWriter.WritePropertyName("winsServers"); jsonWriter.WriteStartArray(); foreach (IPAddress winsServer in scope.WinsServers) jsonWriter.WriteStringValue(winsServer.ToString()); jsonWriter.WriteEndArray(); } if (scope.NtpServers is not null) { jsonWriter.WritePropertyName("ntpServers"); jsonWriter.WriteStartArray(); foreach (IPAddress ntpServer in scope.NtpServers) jsonWriter.WriteStringValue(ntpServer.ToString()); jsonWriter.WriteEndArray(); } if (scope.NtpServerDomainNames is not null) { jsonWriter.WritePropertyName("ntpServerDomainNames"); jsonWriter.WriteStartArray(); foreach (string ntpServerDomainName in scope.NtpServerDomainNames) jsonWriter.WriteStringValue(ntpServerDomainName); jsonWriter.WriteEndArray(); } if (scope.StaticRoutes is not null) { jsonWriter.WritePropertyName("staticRoutes"); jsonWriter.WriteStartArray(); foreach (ClasslessStaticRouteOption.Route route in scope.StaticRoutes) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("destination", route.Destination.ToString()); jsonWriter.WriteString("subnetMask", route.SubnetMask.ToString()); jsonWriter.WriteString("router", route.Router.ToString()); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } if (scope.VendorInfo is not null) { jsonWriter.WritePropertyName("vendorInfo"); jsonWriter.WriteStartArray(); foreach (KeyValuePair entry in scope.VendorInfo) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("identifier", entry.Key); jsonWriter.WriteString("information", BitConverter.ToString(entry.Value.Information).Replace('-', ':')); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } if (scope.CAPWAPAcIpAddresses is not null) { jsonWriter.WritePropertyName("capwapAcIpAddresses"); jsonWriter.WriteStartArray(); foreach (IPAddress acIpAddress in scope.CAPWAPAcIpAddresses) jsonWriter.WriteStringValue(acIpAddress.ToString()); jsonWriter.WriteEndArray(); } if (scope.TftpServerAddresses is not null) { jsonWriter.WritePropertyName("tftpServerAddresses"); jsonWriter.WriteStartArray(); foreach (IPAddress address in scope.TftpServerAddresses) jsonWriter.WriteStringValue(address.ToString()); jsonWriter.WriteEndArray(); } if (scope.GenericOptions is not null) { jsonWriter.WritePropertyName("genericOptions"); jsonWriter.WriteStartArray(); foreach (DhcpOption genericOption in scope.GenericOptions) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("code", (byte)genericOption.Code); jsonWriter.WriteString("value", BitConverter.ToString(genericOption.RawValue).Replace('-', ':')); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } if (scope.Exclusions is not null) { jsonWriter.WritePropertyName("exclusions"); jsonWriter.WriteStartArray(); foreach (Exclusion exclusion in scope.Exclusions) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("startingAddress", exclusion.StartingAddress.ToString()); jsonWriter.WriteString("endingAddress", exclusion.EndingAddress.ToString()); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } jsonWriter.WritePropertyName("reservedLeases"); jsonWriter.WriteStartArray(); foreach (Lease reservedLease in scope.ReservedLeases) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("hostName", reservedLease.HostName); jsonWriter.WriteString("hardwareAddress", BitConverter.ToString(reservedLease.HardwareAddress)); jsonWriter.WriteString("address", reservedLease.Address.ToString()); jsonWriter.WriteString("comments", reservedLease.Comments); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); jsonWriter.WriteBoolean("allowOnlyReservedLeases", scope.AllowOnlyReservedLeases); jsonWriter.WriteBoolean("blockLocallyAdministeredMacAddresses", scope.BlockLocallyAdministeredMacAddresses); jsonWriter.WriteBoolean("ignoreClientIdentifierOption", scope.IgnoreClientIdentifierOption); } public async Task SetDhcpScopeAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string scopeName = request.GetQueryOrForm("name"); string strStartingAddress = request.QueryOrForm("startingAddress"); string strEndingAddress = request.QueryOrForm("endingAddress"); string strSubnetMask = request.QueryOrForm("subnetMask"); bool scopeExists; Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope is null) { //scope does not exists; create new scope if (string.IsNullOrEmpty(strStartingAddress)) throw new DnsWebServiceException("Parameter 'startingAddress' missing."); if (string.IsNullOrEmpty(strEndingAddress)) throw new DnsWebServiceException("Parameter 'endingAddress' missing."); if (string.IsNullOrEmpty(strSubnetMask)) throw new DnsWebServiceException("Parameter 'subnetMask' missing."); scopeExists = false; scope = new Scope(scopeName, true, IPAddress.Parse(strStartingAddress), IPAddress.Parse(strEndingAddress), IPAddress.Parse(strSubnetMask), _dnsWebService._log, _dnsWebService._dhcpServer); scope.IgnoreClientIdentifierOption = true; } else { scopeExists = true; IPAddress startingAddress = string.IsNullOrEmpty(strStartingAddress) ? scope.StartingAddress : IPAddress.Parse(strStartingAddress); IPAddress endingAddress = string.IsNullOrEmpty(strEndingAddress) ? scope.EndingAddress : IPAddress.Parse(strEndingAddress); IPAddress subnetMask = string.IsNullOrEmpty(strSubnetMask) ? scope.SubnetMask : IPAddress.Parse(strSubnetMask); //validate scope address foreach (KeyValuePair entry in _dnsWebService._dhcpServer.Scopes) { Scope existingScope = entry.Value; if (existingScope.Equals(scope)) continue; if (existingScope.IsAddressInRange(startingAddress) || existingScope.IsAddressInRange(endingAddress)) throw new DhcpServerException("Scope with overlapping range already exists: " + existingScope.StartingAddress.ToString() + "-" + existingScope.EndingAddress.ToString()); } scope.ChangeNetwork(startingAddress, endingAddress, subnetMask); } if (request.TryGetQueryOrForm("leaseTimeDays", ushort.Parse, out ushort leaseTimeDays)) scope.LeaseTimeDays = leaseTimeDays; if (request.TryGetQueryOrForm("leaseTimeHours", byte.Parse, out byte leaseTimeHours)) scope.LeaseTimeHours = leaseTimeHours; if (request.TryGetQueryOrForm("leaseTimeMinutes", byte.Parse, out byte leaseTimeMinutes)) scope.LeaseTimeMinutes = leaseTimeMinutes; if (request.TryGetQueryOrForm("offerDelayTime", ushort.Parse, out ushort offerDelayTime)) scope.OfferDelayTime = offerDelayTime; if (request.TryGetQueryOrForm("pingCheckEnabled", bool.Parse, out bool pingCheckEnabled)) scope.PingCheckEnabled = pingCheckEnabled; if (request.TryGetQueryOrForm("pingCheckTimeout", ushort.Parse, out ushort pingCheckTimeout)) scope.PingCheckTimeout = pingCheckTimeout; if (request.TryGetQueryOrForm("pingCheckRetries", byte.Parse, out byte pingCheckRetries)) scope.PingCheckRetries = pingCheckRetries; string domainName = request.QueryOrForm("domainName"); if (domainName is not null) scope.DomainName = domainName.Length == 0 ? null : domainName; string domainSearchList = request.QueryOrForm("domainSearchList"); if (domainSearchList is not null) { if (domainSearchList.Length == 0) scope.DomainSearchList = null; else scope.DomainSearchList = domainSearchList.Split(_commaSeparator, StringSplitOptions.RemoveEmptyEntries); } if (request.TryGetQueryOrForm("dnsUpdates", bool.Parse, out bool dnsUpdates)) scope.DnsUpdates = dnsUpdates; if (request.TryGetQueryOrForm("dnsOverwriteForDynamicLease", bool.Parse, out bool dnsOverwriteForDynamicLease)) scope.DnsOverwriteForDynamicLease = dnsOverwriteForDynamicLease; if (request.TryGetQueryOrForm("dnsTtl", ZoneFile.ParseTtl, out uint dnsTtl)) scope.DnsTtl = dnsTtl; string serverAddress = request.QueryOrForm("serverAddress"); if (serverAddress is not null) scope.ServerAddress = serverAddress.Length == 0 ? null : IPAddress.Parse(serverAddress); string serverHostName = request.QueryOrForm("serverHostName"); if (serverHostName is not null) scope.ServerHostName = serverHostName.Length == 0 ? null : serverHostName; string bootFileName = request.QueryOrForm("bootFileName"); if (bootFileName is not null) scope.BootFileName = bootFileName.Length == 0 ? null : bootFileName; string routerAddress = request.QueryOrForm("routerAddress"); if (routerAddress is not null) scope.RouterAddress = routerAddress.Length == 0 ? null : IPAddress.Parse(routerAddress); if (request.TryGetQueryOrForm("useThisDnsServer", bool.Parse, out bool useThisDnsServer)) scope.UseThisDnsServer = useThisDnsServer; if (!scope.UseThisDnsServer) { string dnsServers = request.QueryOrForm("dnsServers"); if (dnsServers is not null) { if (dnsServers.Length == 0) scope.DnsServers = null; else scope.DnsServers = dnsServers.Split(IPAddress.Parse, ','); } } string winsServers = request.QueryOrForm("winsServers"); if (winsServers is not null) { if (winsServers.Length == 0) scope.WinsServers = null; else scope.WinsServers = winsServers.Split(IPAddress.Parse, ','); } string ntpServers = request.QueryOrForm("ntpServers"); if (ntpServers is not null) { if (ntpServers.Length == 0) scope.NtpServers = null; else scope.NtpServers = ntpServers.Split(IPAddress.Parse, ','); } string ntpServerDomainNames = request.QueryOrForm("ntpServerDomainNames"); if (ntpServerDomainNames is not null) { if (ntpServerDomainNames.Length == 0) scope.NtpServerDomainNames = null; else scope.NtpServerDomainNames = ntpServerDomainNames.Split(_commaSeparator, StringSplitOptions.RemoveEmptyEntries); } string strStaticRoutes = request.QueryOrForm("staticRoutes"); if (strStaticRoutes is not null) { if (strStaticRoutes.Length == 0) { scope.StaticRoutes = null; } else { string[] strStaticRoutesParts = strStaticRoutes.Split('|'); List staticRoutes = new List(); for (int i = 0; i < strStaticRoutesParts.Length; i += 3) staticRoutes.Add(new ClasslessStaticRouteOption.Route(IPAddress.Parse(strStaticRoutesParts[i + 0]), IPAddress.Parse(strStaticRoutesParts[i + 1]), IPAddress.Parse(strStaticRoutesParts[i + 2]))); scope.StaticRoutes = staticRoutes; } } string strVendorInfo = request.QueryOrForm("vendorInfo"); if (strVendorInfo is not null) { if (strVendorInfo.Length == 0) { scope.VendorInfo = null; } else { string[] strVendorInfoParts = strVendorInfo.Split('|'); Dictionary vendorInfo = new Dictionary(); for (int i = 0; i < strVendorInfoParts.Length; i += 2) vendorInfo.Add(strVendorInfoParts[i + 0], new VendorSpecificInformationOption(strVendorInfoParts[i + 1])); scope.VendorInfo = vendorInfo; } } string capwapAcIpAddresses = request.QueryOrForm("capwapAcIpAddresses"); if (capwapAcIpAddresses is not null) { if (capwapAcIpAddresses.Length == 0) scope.CAPWAPAcIpAddresses = null; else scope.CAPWAPAcIpAddresses = capwapAcIpAddresses.Split(IPAddress.Parse, ','); } string tftpServerAddresses = request.QueryOrForm("tftpServerAddresses"); if (tftpServerAddresses is not null) { if (tftpServerAddresses.Length == 0) scope.TftpServerAddresses = null; else scope.TftpServerAddresses = tftpServerAddresses.Split(IPAddress.Parse, ','); } string strGenericOptions = request.QueryOrForm("genericOptions"); if (strGenericOptions is not null) { if (strGenericOptions.Length == 0) { scope.GenericOptions = null; } else { string[] strGenericOptionsParts = strGenericOptions.Split('|'); List genericOptions = new List(); for (int i = 0; i < strGenericOptionsParts.Length; i += 2) genericOptions.Add(new DhcpOption((DhcpOptionCode)byte.Parse(strGenericOptionsParts[i + 0]), strGenericOptionsParts[i + 1])); scope.GenericOptions = genericOptions; } } string strExclusions = request.QueryOrForm("exclusions"); if (strExclusions is not null) { if (strExclusions.Length == 0) { scope.Exclusions = null; } else { string[] strExclusionsParts = strExclusions.Split('|'); List exclusions = new List(); for (int i = 0; i < strExclusionsParts.Length; i += 2) exclusions.Add(new Exclusion(IPAddress.Parse(strExclusionsParts[i + 0]), IPAddress.Parse(strExclusionsParts[i + 1]))); scope.Exclusions = exclusions; } } string strReservedLeases = request.QueryOrForm("reservedLeases"); if (strReservedLeases is not null) { if (strReservedLeases.Length == 0) { scope.ReservedLeases = null; } else { string[] strReservedLeaseParts = strReservedLeases.Split('|'); List reservedLeases = new List(); for (int i = 0; i < strReservedLeaseParts.Length; i += 4) reservedLeases.Add(new Lease(LeaseType.Reserved, strReservedLeaseParts[i + 0], DhcpMessageHardwareAddressType.Ethernet, strReservedLeaseParts[i + 1], IPAddress.Parse(strReservedLeaseParts[i + 2]), strReservedLeaseParts[i + 3])); scope.ReservedLeases = reservedLeases; } } if (request.TryGetQueryOrForm("allowOnlyReservedLeases", bool.Parse, out bool allowOnlyReservedLeases)) scope.AllowOnlyReservedLeases = allowOnlyReservedLeases; if (request.TryGetQueryOrForm("blockLocallyAdministeredMacAddresses", bool.Parse, out bool blockLocallyAdministeredMacAddresses)) scope.BlockLocallyAdministeredMacAddresses = blockLocallyAdministeredMacAddresses; if (request.TryGetQueryOrForm("ignoreClientIdentifierOption", bool.Parse, out bool ignoreClientIdentifierOption)) scope.IgnoreClientIdentifierOption = ignoreClientIdentifierOption; if (scopeExists) { _dnsWebService._dhcpServer.SaveScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope was updated successfully: " + scopeName); } else { await _dnsWebService._dhcpServer.AddScopeAsync(scope); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope was added successfully: " + scopeName); } if (request.TryGetQueryOrForm("newName", out string newName) && !newName.Equals(scopeName)) { _dnsWebService._dhcpServer.RenameScope(scopeName, newName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope was renamed successfully: '" + scopeName + "' to '" + newName + "'"); } } public void AddReservedLease(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("No such scope exists: " + scopeName); string hostName = request.QueryOrForm("hostName"); string hardwareAddress = request.GetQueryOrForm("hardwareAddress"); string strIpAddress = request.GetQueryOrForm("ipAddress"); string comments = request.QueryOrForm("comments"); Lease reservedLease = new Lease(LeaseType.Reserved, hostName, DhcpMessageHardwareAddressType.Ethernet, hardwareAddress, IPAddress.Parse(strIpAddress), comments); if (!scope.AddReservedLease(reservedLease)) throw new DnsWebServiceException("Failed to add reserved lease for scope: " + scopeName); _dnsWebService._dhcpServer.SaveScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope reserved lease was added successfully: " + scopeName); } public void RemoveReservedLease(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("No such scope exists: " + scopeName); string hardwareAddress = request.GetQueryOrForm("hardwareAddress"); if (!scope.RemoveReservedLease(hardwareAddress)) throw new DnsWebServiceException("Failed to remove reserved lease for scope: " + scopeName); _dnsWebService._dhcpServer.SaveScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope reserved lease was removed successfully: " + scopeName); } public async Task EnableDhcpScopeAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string scopeName = context.Request.GetQueryOrForm("name"); await _dnsWebService._dhcpServer.EnableScopeAsync(scopeName, true); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope was enabled successfully: " + scopeName); } public void DisableDhcpScope(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string scopeName = context.Request.GetQueryOrForm("name"); _dnsWebService._dhcpServer.DisableScope(scopeName, true); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope was disabled successfully: " + scopeName); } public void DeleteDhcpScope(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string scopeName = context.Request.GetQueryOrForm("name"); _dnsWebService._dhcpServer.DeleteScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope was deleted successfully: " + scopeName); } public void RemoveDhcpLease(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope is null) throw new DnsWebServiceException("DHCP scope does not exists: " + scopeName); string clientIdentifier = request.QueryOrForm("clientIdentifier"); string hardwareAddress = request.QueryOrForm("hardwareAddress"); if (!string.IsNullOrEmpty(clientIdentifier)) scope.RemoveLease(ClientIdentifierOption.Parse(clientIdentifier)); else if (!string.IsNullOrEmpty(hardwareAddress)) scope.RemoveLease(hardwareAddress); else throw new DnsWebServiceException("Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified."); _dnsWebService._dhcpServer.SaveScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope's lease was removed successfully: " + scopeName); } public void ConvertToReservedLease(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope == null) throw new DnsWebServiceException("DHCP scope does not exists: " + scopeName); string clientIdentifier = request.QueryOrForm("clientIdentifier"); string hardwareAddress = request.QueryOrForm("hardwareAddress"); if (!string.IsNullOrEmpty(clientIdentifier)) scope.ConvertToReservedLease(ClientIdentifierOption.Parse(clientIdentifier)); else if (!string.IsNullOrEmpty(hardwareAddress)) scope.ConvertToReservedLease(hardwareAddress); else throw new DnsWebServiceException("Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified."); _dnsWebService._dhcpServer.SaveScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope's lease was reserved successfully: " + scopeName); } public void ConvertToDynamicLease(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.DhcpServer, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string scopeName = request.GetQueryOrForm("name"); Scope scope = _dnsWebService._dhcpServer.GetScope(scopeName); if (scope == null) throw new DnsWebServiceException("DHCP scope does not exists: " + scopeName); string clientIdentifier = request.QueryOrForm("clientIdentifier"); string hardwareAddress = request.QueryOrForm("hardwareAddress"); if (!string.IsNullOrEmpty(clientIdentifier)) scope.ConvertToDynamicLease(ClientIdentifierOption.Parse(clientIdentifier)); else if (!string.IsNullOrEmpty(hardwareAddress)) scope.ConvertToDynamicLease(hardwareAddress); else throw new DnsWebServiceException("Parameter 'hardwareAddress' or 'clientIdentifier' missing. At least one of them must be specified."); _dnsWebService._dhcpServer.SaveScope(scopeName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DHCP scope's lease was unreserved successfully: " + scopeName); } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceLogsApi.cs ================================================ /* 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.ApplicationCommon; using DnsServerCore.Auth; using DnsServerCore.Dns.Applications; using Microsoft.AspNetCore.Http; using System; using System.Globalization; using System.IO; using System.Net; using System.Text; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore { public partial class DnsWebService { class WebServiceLogsApi { #region variables readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceLogsApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region public public void ListLogs(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); string[] logFiles = _dnsWebService._log.ListLogFiles(); Array.Sort(logFiles); Array.Reverse(logFiles); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("logFiles"); jsonWriter.WriteStartArray(); foreach (string logFile in logFiles) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("fileName", Path.GetFileNameWithoutExtension(logFile)); jsonWriter.WriteString("size", WebUtilities.GetFormattedSize(new FileInfo(logFile).Length)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public Task DownloadLogAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string fileName = request.GetQueryOrForm("fileName"); int limit = request.GetQueryOrForm("limit", int.Parse, 0); return _dnsWebService._log.DownloadLogFileAsync(context, fileName, limit * 1024 * 1024); } public void DeleteLog(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string log = request.GetQueryOrForm("log"); _dnsWebService._log.DeleteLogFile(log); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Log file was deleted: " + log); } public void DeleteAllLogs(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._log.DeleteAllLogFiles(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] All log files were deleted."); } public void DeleteAllStats(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.StatsManager.DeleteAllStats(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] All stats files were deleted."); } public async Task QueryLogsAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name"); string classPath = request.GetQueryOrForm("classPath"); if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); if (!application.DnsQueryLogs.TryGetValue(classPath, out IDnsQueryLogs queryLogs)) throw new DnsWebServiceException("DNS application '" + classPath + "' class path was not found: " + name); long pageNumber = request.GetQueryOrForm("pageNumber", long.Parse, 1); int entriesPerPage = request.GetQueryOrForm("entriesPerPage", int.Parse, 25); bool descendingOrder = request.GetQueryOrForm("descendingOrder", bool.Parse, true); DateTime? start = null; string strStart = request.QueryOrForm("start"); if (!string.IsNullOrEmpty(strStart)) start = DateTime.Parse(strStart, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); DateTime? end = null; string strEnd = request.QueryOrForm("end"); if (!string.IsNullOrEmpty(strEnd)) end = DateTime.Parse(strEnd, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); IPAddress clientIpAddress = request.GetQueryOrForm("clientIpAddress", IPAddress.Parse, null); DnsTransportProtocol? protocol = null; string strProtocol = request.QueryOrForm("protocol"); if (!string.IsNullOrEmpty(strProtocol)) protocol = Enum.Parse(strProtocol, true); DnsServerResponseType? responseType = null; string strResponseType = request.QueryOrForm("responseType"); if (!string.IsNullOrEmpty(strResponseType)) responseType = Enum.Parse(strResponseType, true); DnsResponseCode? rcode = null; string strRcode = request.QueryOrForm("rcode"); if (!string.IsNullOrEmpty(strRcode)) rcode = Enum.Parse(strRcode, true); string qname = request.GetQueryOrForm("qname", null); if (qname is not null) qname = qname.TrimEnd('.'); DnsResourceRecordType? qtype = null; string strQtype = request.QueryOrForm("qtype"); if (!string.IsNullOrEmpty(strQtype)) qtype = Enum.Parse(strQtype, true); DnsClass? qclass = null; string strQclass = request.QueryOrForm("qclass"); if (!string.IsNullOrEmpty(strQclass)) qclass = Enum.Parse(strQclass, true); DnsLogPage page = await queryLogs.QueryLogsAsync(pageNumber, entriesPerPage, descendingOrder, start, end, clientIpAddress, protocol, responseType, rcode, qname, qtype, qclass); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteNumber("pageNumber", page.PageNumber); jsonWriter.WriteNumber("totalPages", page.TotalPages); jsonWriter.WriteNumber("totalEntries", page.TotalEntries); jsonWriter.WritePropertyName("entries"); jsonWriter.WriteStartArray(); foreach (DnsLogEntry entry in page.Entries) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("rowNumber", entry.RowNumber); jsonWriter.WriteString("timestamp", entry.Timestamp); jsonWriter.WriteString("clientIpAddress", entry.ClientIpAddress.ToString()); jsonWriter.WriteString("protocol", entry.Protocol.ToString()); jsonWriter.WriteString("responseType", entry.ResponseType.ToString()); if (entry.ResponseRtt.HasValue) jsonWriter.WriteNumber("responseRtt", entry.ResponseRtt.Value); jsonWriter.WriteString("rcode", entry.RCODE.ToString()); jsonWriter.WriteString("qname", entry.Question?.Name); jsonWriter.WriteString("qtype", entry.Question?.Type.ToString()); jsonWriter.WriteString("qclass", entry.Question?.Class.ToString()); jsonWriter.WriteString("answer", entry.Answer); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } public async Task ExportLogsAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Logs, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string name = request.GetQueryOrForm("name"); string classPath = request.GetQueryOrForm("classPath"); if (!_dnsWebService._dnsServer.DnsApplicationManager.Applications.TryGetValue(name, out DnsApplication application)) throw new DnsWebServiceException("DNS application was not found: " + name); if (!application.DnsQueryLogs.TryGetValue(classPath, out IDnsQueryLogs queryLogs)) throw new DnsWebServiceException("DNS application '" + classPath + "' class path was not found: " + name); DateTime? start = null; string strStart = request.QueryOrForm("start"); if (!string.IsNullOrEmpty(strStart)) start = DateTime.Parse(strStart, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); DateTime? end = null; string strEnd = request.QueryOrForm("end"); if (!string.IsNullOrEmpty(strEnd)) end = DateTime.Parse(strEnd, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); IPAddress clientIpAddress = request.GetQueryOrForm("clientIpAddress", IPAddress.Parse, null); DnsTransportProtocol? protocol = null; string strProtocol = request.QueryOrForm("protocol"); if (!string.IsNullOrEmpty(strProtocol)) protocol = Enum.Parse(strProtocol, true); DnsServerResponseType? responseType = null; string strResponseType = request.QueryOrForm("responseType"); if (!string.IsNullOrEmpty(strResponseType)) responseType = Enum.Parse(strResponseType, true); DnsResponseCode? rcode = null; string strRcode = request.QueryOrForm("rcode"); if (!string.IsNullOrEmpty(strRcode)) rcode = Enum.Parse(strRcode, true); string qname = request.GetQueryOrForm("qname", null); DnsResourceRecordType? qtype = null; string strQtype = request.QueryOrForm("qtype"); if (!string.IsNullOrEmpty(strQtype)) qtype = Enum.Parse(strQtype, true); DnsClass? qclass = null; string strQclass = request.QueryOrForm("qclass"); if (!string.IsNullOrEmpty(strQclass)) qclass = Enum.Parse(strQclass, true); static async Task WriteCsvFieldAsync(StreamWriter sW, string data) { if ((data is null) || (data.Length == 0)) return; if (data.Contains('"', StringComparison.OrdinalIgnoreCase)) { await sW.WriteAsync('"'); await sW.WriteAsync(data.Replace("\"", "\"\"")); await sW.WriteAsync('"'); } else if (data.Contains(',', StringComparison.OrdinalIgnoreCase) || data.Contains(' ', StringComparison.OrdinalIgnoreCase)) { await sW.WriteAsync('"'); await sW.WriteAsync(data); await sW.WriteAsync('"'); } else { await sW.WriteAsync(data); } } DnsLogPage page; long pageNumber = 1; string tmpFile = Path.GetTempFileName(); try { using (FileStream csvFileStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { StreamWriter sW = new StreamWriter(csvFileStream, Encoding.UTF8); await sW.WriteLineAsync("RowNumber,Timestamp,ClientIpAddress,Protocol,ResponseType,ResponseRtt,RCODE,Domain,Type,Class,Answer"); do { page = await queryLogs.QueryLogsAsync(pageNumber, 10000, false, start, end, clientIpAddress, protocol, responseType, rcode, qname, qtype, qclass); foreach (DnsLogEntry entry in page.Entries) { await WriteCsvFieldAsync(sW, entry.RowNumber.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.Timestamp.ToString("O")); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.ClientIpAddress.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.Protocol.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.ResponseType.ToString()); await sW.WriteAsync(','); if (entry.ResponseRtt.HasValue) await WriteCsvFieldAsync(sW, entry.ResponseRtt.Value.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.RCODE.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.Question?.Name.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.Question?.Type.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.Question?.Class.ToString()); await sW.WriteAsync(','); await WriteCsvFieldAsync(sW, entry.Answer); await sW.WriteLineAsync(); } } while (pageNumber++ < page.TotalPages); await sW.FlushAsync(); //send csv file csvFileStream.Position = 0; HttpResponse response = context.Response; response.ContentType = "text/csv"; response.ContentLength = csvFileStream.Length; response.Headers.ContentDisposition = "attachment;filename=" + _dnsWebService._dnsServer.ServerDomain + DateTime.UtcNow.ToString("_yyyy-MM-dd_HH-mm-ss") + "_query_logs.csv"; using (Stream output = response.Body) { await csvFileStream.CopyToAsync(output); } } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceOtherZonesApi.cs ================================================ /* 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.Dns.Zones; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.IO; using System.Net; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore { public partial class DnsWebService { class WebServiceOtherZonesApi { #region variables readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceOtherZonesApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region public #region cache api public void FlushCache(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.CacheZoneManager.Flush(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Cache was flushed."); } public void ListCachedZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain", ""); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string direction = request.QueryOrForm("direction"); if (direction is not null) direction = direction.ToLowerInvariant(); List subZones = new List(); List records = new List(); while (true) { subZones.Clear(); records.Clear(); _dnsWebService._dnsServer.CacheZoneManager.ListSubDomains(domain, subZones); _dnsWebService._dnsServer.CacheZoneManager.ListAllRecords(domain, records); if (records.Count > 0) break; if (subZones.Count != 1) break; if (direction == "up") { if (domain.Length == 0) break; int i = domain.IndexOf('.'); if (i < 0) domain = ""; else domain = domain.Substring(i + 1); } else if (domain.Length == 0) { domain = subZones[0]; } else { domain = subZones[0] + "." + domain; } } subZones.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("domain", domain); if (DnsClient.TryConvertDomainNameToUnicode(domain, out string idn)) jsonWriter.WriteString("domainIdn", idn); jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); if (domain.Length != 0) domain = "." + domain; foreach (string subZone in subZones) { string zone = subZone + domain; if (DnsClient.TryConvertDomainNameToUnicode(zone, out string zoneIdn)) zone = zoneIdn; jsonWriter.WriteStringValue(zone); } jsonWriter.WriteEndArray(); WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, false); } public void DeleteCachedZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Cache, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string domain = context.Request.GetQueryOrForm("domain"); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); if (_dnsWebService._dnsServer.CacheZoneManager.DeleteZone(domain)) _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Cached zone was deleted: " + domain); } #endregion #region allowed zones api public void ListAllowedZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain", ""); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string direction = request.QueryOrForm("direction"); if (direction is not null) direction = direction.ToLowerInvariant(); List subZones = new List(); List records = new List(); while (true) { subZones.Clear(); records.Clear(); _dnsWebService._dnsServer.AllowedZoneManager.ListSubDomains(domain, subZones); _dnsWebService._dnsServer.AllowedZoneManager.ListAllRecords(domain, records); if (records.Count > 0) break; if (subZones.Count != 1) break; if (direction == "up") { if (domain.Length == 0) break; int i = domain.IndexOf('.'); if (i < 0) domain = ""; else domain = domain.Substring(i + 1); } else if (domain.Length == 0) { domain = subZones[0]; } else { domain = subZones[0] + "." + domain; } } subZones.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("domain", domain); if (DnsClient.TryConvertDomainNameToUnicode(domain, out string idn)) jsonWriter.WriteString("domainIdn", idn); jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); if (domain.Length != 0) domain = "." + domain; foreach (string subZone in subZones) { string zone = subZone + domain; if (DnsClient.TryConvertDomainNameToUnicode(zone, out string zoneIdn)) zone = zoneIdn; jsonWriter.WriteStringValue(zone); } jsonWriter.WriteEndArray(); WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, true); } public void ImportAllowedZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string allowedZones = request.GetQueryOrForm("allowedZones"); string[] allowedZonesList = allowedZones.Split(','); for (int i = 0; i < allowedZonesList.Length; i++) { if (DnsClient.IsDomainNameUnicode(allowedZonesList[i])) allowedZonesList[i] = DnsClient.ConvertDomainNameToAscii(allowedZonesList[i]); } _dnsWebService._dnsServer.AllowedZoneManager.ImportZones(allowedZonesList); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Total " + allowedZonesList.Length + " zones were imported into allowed zone successfully."); _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public async Task ExportAllowedZonesAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); IReadOnlyList zoneInfoList = _dnsWebService._dnsServer.AllowedZoneManager.GetAllZones(); HttpResponse response = context.Response; response.ContentType = "text/plain"; response.Headers.ContentDisposition = "attachment;filename=AllowedZones.txt"; await using (StreamWriter sW = new StreamWriter(response.Body)) { foreach (AuthZoneInfo zoneInfo in zoneInfoList) await sW.WriteLineAsync(zoneInfo.Name); } } public void DeleteAllowedZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string domain = context.Request.GetQueryOrForm("domain"); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); if (_dnsWebService._dnsServer.AllowedZoneManager.DeleteZone(domain)) { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Allowed zone was deleted: " + domain); _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } } public void FlushAllowedZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.AllowedZoneManager.Flush(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Allowed zone was flushed successfully."); _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void AllowZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Allowed, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string domain = context.Request.GetQueryOrForm("domain"); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); if (IPAddress.TryParse(domain, out IPAddress ipAddress)) domain = ipAddress.GetReverseDomain(); if (_dnsWebService._dnsServer.AllowedZoneManager.AllowZone(domain)) { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Zone was allowed: " + domain); _dnsWebService._dnsServer.AllowedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } } #endregion #region blocked zones api public void ListBlockedZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain", ""); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string direction = request.QueryOrForm("direction"); if (direction is not null) direction = direction.ToLowerInvariant(); List subZones = new List(); List records = new List(); while (true) { subZones.Clear(); records.Clear(); _dnsWebService._dnsServer.BlockedZoneManager.ListSubDomains(domain, subZones); _dnsWebService._dnsServer.BlockedZoneManager.ListAllRecords(domain, records); if (records.Count > 0) break; if (subZones.Count != 1) break; if (direction == "up") { if (domain.Length == 0) break; int i = domain.IndexOf('.'); if (i < 0) domain = ""; else domain = domain.Substring(i + 1); } else if (domain.Length == 0) { domain = subZones[0]; } else { domain = subZones[0] + "." + domain; } } subZones.Sort(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("domain", domain); if (DnsClient.TryConvertDomainNameToUnicode(domain, out string idn)) jsonWriter.WriteString("domainIdn", idn); jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); if (domain.Length != 0) domain = "." + domain; foreach (string subZone in subZones) { string zone = subZone + domain; if (DnsClient.TryConvertDomainNameToUnicode(zone, out string zoneIdn)) zone = zoneIdn; jsonWriter.WriteStringValue(zone); } jsonWriter.WriteEndArray(); WebServiceZonesApi.WriteRecordsAsJson(records, jsonWriter, true); } public void ImportBlockedZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string blockedZones = request.GetQueryOrForm("blockedZones"); string[] blockedZonesList = blockedZones.Split(','); for (int i = 0; i < blockedZonesList.Length; i++) { if (DnsClient.IsDomainNameUnicode(blockedZonesList[i])) blockedZonesList[i] = DnsClient.ConvertDomainNameToAscii(blockedZonesList[i]); } _dnsWebService._dnsServer.BlockedZoneManager.ImportZones(blockedZonesList); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Total " + blockedZonesList.Length + " zones were imported into blocked zone successfully."); _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public async Task ExportBlockedZonesAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); IReadOnlyList zoneInfoList = _dnsWebService._dnsServer.BlockedZoneManager.GetAllZones(); HttpResponse response = context.Response; response.ContentType = "text/plain"; response.Headers.ContentDisposition = "attachment;filename=BlockedZones.txt"; await using (StreamWriter sW = new StreamWriter(response.Body)) { foreach (AuthZoneInfo zoneInfo in zoneInfoList) await sW.WriteLineAsync(zoneInfo.Name); } } public void DeleteBlockedZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string domain = context.Request.GetQueryOrForm("domain"); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); if (_dnsWebService._dnsServer.BlockedZoneManager.DeleteZone(domain)) { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Blocked zone was deleted: " + domain); _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } } public void FlushBlockedZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.BlockedZoneManager.Flush(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Blocked zone was flushed successfully."); _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } public void BlockZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Blocked, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string domain = context.Request.GetQueryOrForm("domain"); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); if (IPAddress.TryParse(domain, out IPAddress ipAddress)) domain = ipAddress.GetReverseDomain(); if (_dnsWebService._dnsServer.BlockedZoneManager.BlockZone(domain)) { _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Domain was added to blocked zone: " + domain); _dnsWebService._dnsServer.BlockedZoneManager.SaveZoneFile(); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); } } #endregion #endregion } } } ================================================ FILE: DnsServerCore/WebServiceSettingsApi.cs ================================================ /* 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.Dns; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Net; using System.Net.Mail; using System.Net.Sockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ClientConnection; using TechnitiumLibrary.Net.Dns.ResourceRecords; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore { public partial class DnsWebService { sealed class WebServiceSettingsApi { #region variables readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceSettingsApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region private private void RestartService(bool restartDnsService, bool restartWebService, IReadOnlyList oldWebServiceLocalAddresses, int oldWebServiceHttpPort, int oldWebServiceTlsPort) { if (restartDnsService) { ThreadPool.QueueUserWorkItem(async delegate (object state) { try { _dnsWebService._log.Write("Attempting to restart DNS service."); await _dnsWebService._dnsServer.StopAsync(); await _dnsWebService._dnsServer.StartAsync(); _dnsWebService._log.Write("DNS service was restarted successfully."); } catch (Exception ex) { _dnsWebService._log.Write("Failed to restart DNS service.\r\n" + ex.ToString()); } }); } if (restartWebService) { ThreadPool.QueueUserWorkItem(async delegate (object state) { try { await Task.Delay(2000); //wait for this HTTP response to be delivered before stopping web server _dnsWebService._log.Write("Attempting to restart web service."); try { await _dnsWebService.StopWebServiceAsync(); await _dnsWebService.TryStartWebServiceAsync(oldWebServiceLocalAddresses, oldWebServiceHttpPort, oldWebServiceTlsPort); _dnsWebService._log.Write("Web service was restarted successfully."); } catch (Exception ex) { _dnsWebService._log.Write("Failed to restart web service.\r\n" + ex.ToString()); } //update cluster node URL to reflect latest TLS port if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.UpdateSelfNodeUrlAndCertificate(); } catch (Exception ex) { _dnsWebService._log.Write(ex); } }); } } private void WriteDnsSettings(Utf8JsonWriter jsonWriter) { //info jsonWriter.WriteString("version", _dnsWebService.GetServerVersion()); jsonWriter.WriteString("uptimestamp", _dnsWebService._uptimestamp); jsonWriter.WriteBoolean("clusterInitialized", _dnsWebService._clusterManager.ClusterInitialized); if (_dnsWebService._clusterManager.ClusterInitialized) { jsonWriter.WriteString("clusterDomain", _dnsWebService._clusterManager.ClusterDomain); _dnsWebService._clusterApi.WriteClusterNodes(jsonWriter); } //general jsonWriter.WriteString("dnsServerDomain", _dnsWebService._dnsServer.ServerDomain); jsonWriter.WriteStringArray("dnsServerLocalEndPoints", _dnsWebService._dnsServer.LocalEndPoints); jsonWriter.WriteStringArray("dnsServerIPv4SourceAddresses", DnsClientConnection.IPv4SourceAddresses); jsonWriter.WriteStringArray("dnsServerIPv6SourceAddresses", DnsClientConnection.IPv6SourceAddresses); jsonWriter.WriteNumber("defaultRecordTtl", _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl); jsonWriter.WriteNumber("defaultNsRecordTtl", _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl); jsonWriter.WriteNumber("defaultSoaRecordTtl", _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl); jsonWriter.WriteString("defaultResponsiblePerson", _dnsWebService._dnsServer.DefaultResponsiblePerson?.Address); jsonWriter.WriteBoolean("useSoaSerialDateScheme", _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme); jsonWriter.WriteNumber("minSoaRefresh", _dnsWebService._dnsServer.AuthZoneManager.MinSoaRefresh); jsonWriter.WriteNumber("minSoaRetry", _dnsWebService._dnsServer.AuthZoneManager.MinSoaRetry); jsonWriter.WriteStringArray("zoneTransferAllowedNetworks", _dnsWebService._dnsServer.ZoneTransferAllowedNetworks); jsonWriter.WriteStringArray("notifyAllowedNetworks", _dnsWebService._dnsServer.NotifyAllowedNetworks); jsonWriter.WriteBoolean("dnsAppsEnableAutomaticUpdate", _dnsWebService._dnsServer.DnsApplicationManager.EnableAutomaticUpdate); jsonWriter.WriteBoolean("preferIPv6", _dnsWebService._dnsServer.PreferIPv6); jsonWriter.WriteBoolean("enableUdpSocketPool", _dnsWebService._dnsServer.EnableUdpSocketPool); jsonWriter.WriteStartArray("socketPoolExcludedPorts"); ushort[] socketPoolExcludedPorts = UdpClientConnection.SocketPoolExcludedPorts; if (socketPoolExcludedPorts is not null) { foreach (ushort excludedPort in socketPoolExcludedPorts) jsonWriter.WriteNumberValue(excludedPort); } jsonWriter.WriteEndArray(); jsonWriter.WriteNumber("udpPayloadSize", _dnsWebService._dnsServer.UdpPayloadSize); jsonWriter.WriteBoolean("dnssecValidation", _dnsWebService._dnsServer.DnssecValidation); jsonWriter.WriteBoolean("eDnsClientSubnet", _dnsWebService._dnsServer.EDnsClientSubnet); jsonWriter.WriteNumber("eDnsClientSubnetIPv4PrefixLength", _dnsWebService._dnsServer.EDnsClientSubnetIPv4PrefixLength); jsonWriter.WriteNumber("eDnsClientSubnetIPv6PrefixLength", _dnsWebService._dnsServer.EDnsClientSubnetIPv6PrefixLength); jsonWriter.WriteString("eDnsClientSubnetIpv4Override", _dnsWebService._dnsServer.EDnsClientSubnetIpv4Override?.ToString()); jsonWriter.WriteString("eDnsClientSubnetIpv6Override", _dnsWebService._dnsServer.EDnsClientSubnetIpv6Override?.ToString()); jsonWriter.WriteStartArray("qpmPrefixLimitsIPv4"); foreach (KeyValuePair qpmPrefixLimit in _dnsWebService._dnsServer.QpmPrefixLimitsIPv4) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("prefix", qpmPrefixLimit.Key); jsonWriter.WriteNumber("udpLimit", qpmPrefixLimit.Value.Item1); jsonWriter.WriteNumber("tcpLimit", qpmPrefixLimit.Value.Item2); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); jsonWriter.WriteStartArray("qpmPrefixLimitsIPv6"); foreach (KeyValuePair qpmPrefixLimit in _dnsWebService._dnsServer.QpmPrefixLimitsIPv6) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("prefix", qpmPrefixLimit.Key); jsonWriter.WriteNumber("udpLimit", qpmPrefixLimit.Value.Item1); jsonWriter.WriteNumber("tcpLimit", qpmPrefixLimit.Value.Item2); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); jsonWriter.WriteNumber("qpmLimitSampleMinutes", _dnsWebService._dnsServer.QpmLimitSampleMinutes); jsonWriter.WriteNumber("qpmLimitUdpTruncationPercentage", _dnsWebService._dnsServer.QpmLimitUdpTruncationPercentage); jsonWriter.WritePropertyName("qpmLimitBypassList"); jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.QpmLimitBypassList is not null) { foreach (NetworkAddress network in _dnsWebService._dnsServer.QpmLimitBypassList) jsonWriter.WriteStringValue(network.ToString()); } jsonWriter.WriteEndArray(); jsonWriter.WriteNumber("clientTimeout", _dnsWebService._dnsServer.ClientTimeout); jsonWriter.WriteNumber("tcpSendTimeout", _dnsWebService._dnsServer.TcpSendTimeout); jsonWriter.WriteNumber("tcpReceiveTimeout", _dnsWebService._dnsServer.TcpReceiveTimeout); jsonWriter.WriteNumber("quicIdleTimeout", _dnsWebService._dnsServer.QuicIdleTimeout); jsonWriter.WriteNumber("quicMaxInboundStreams", _dnsWebService._dnsServer.QuicMaxInboundStreams); jsonWriter.WriteNumber("listenBacklog", _dnsWebService._dnsServer.ListenBacklog); jsonWriter.WriteNumber("maxConcurrentResolutionsPerCore", _dnsWebService._dnsServer.MaxConcurrentResolutionsPerCore); //web service jsonWriter.WritePropertyName("webServiceLocalAddresses"); jsonWriter.WriteStartArray(); foreach (IPAddress localAddress in _dnsWebService._webServiceLocalAddresses) { if (localAddress.AddressFamily == AddressFamily.InterNetworkV6) jsonWriter.WriteStringValue("[" + localAddress.ToString() + "]"); else jsonWriter.WriteStringValue(localAddress.ToString()); } jsonWriter.WriteEndArray(); jsonWriter.WriteNumber("webServiceHttpPort", _dnsWebService._webServiceHttpPort); jsonWriter.WriteBoolean("webServiceEnableTls", _dnsWebService._webServiceEnableTls); jsonWriter.WriteBoolean("webServiceEnableHttp3", _dnsWebService._webServiceEnableHttp3); jsonWriter.WriteBoolean("webServiceHttpToTlsRedirect", _dnsWebService._webServiceHttpToTlsRedirect); jsonWriter.WriteBoolean("webServiceUseSelfSignedTlsCertificate", _dnsWebService._webServiceUseSelfSignedTlsCertificate); jsonWriter.WriteNumber("webServiceTlsPort", _dnsWebService._webServiceTlsPort); jsonWriter.WriteString("webServiceTlsCertificatePath", _dnsWebService._webServiceTlsCertificatePath); jsonWriter.WriteString("webServiceTlsCertificatePassword", "************"); jsonWriter.WriteString("webServiceRealIpHeader", _dnsWebService._webServiceRealIpHeader); //optional protocols jsonWriter.WriteBoolean("enableDnsOverUdpProxy", _dnsWebService._dnsServer.EnableDnsOverUdpProxy); jsonWriter.WriteBoolean("enableDnsOverTcpProxy", _dnsWebService._dnsServer.EnableDnsOverTcpProxy); jsonWriter.WriteBoolean("enableDnsOverHttp", _dnsWebService._dnsServer.EnableDnsOverHttp); jsonWriter.WriteBoolean("enableDnsOverTls", _dnsWebService._dnsServer.EnableDnsOverTls); jsonWriter.WriteBoolean("enableDnsOverHttps", _dnsWebService._dnsServer.EnableDnsOverHttps); jsonWriter.WriteBoolean("enableDnsOverHttp3", _dnsWebService._dnsServer.EnableDnsOverHttp3); jsonWriter.WriteBoolean("enableDnsOverQuic", _dnsWebService._dnsServer.EnableDnsOverQuic); jsonWriter.WriteNumber("dnsOverUdpProxyPort", _dnsWebService._dnsServer.DnsOverUdpProxyPort); jsonWriter.WriteNumber("dnsOverTcpProxyPort", _dnsWebService._dnsServer.DnsOverTcpProxyPort); jsonWriter.WriteNumber("dnsOverHttpPort", _dnsWebService._dnsServer.DnsOverHttpPort); jsonWriter.WriteNumber("dnsOverTlsPort", _dnsWebService._dnsServer.DnsOverTlsPort); jsonWriter.WriteNumber("dnsOverHttpsPort", _dnsWebService._dnsServer.DnsOverHttpsPort); jsonWriter.WriteNumber("dnsOverQuicPort", _dnsWebService._dnsServer.DnsOverQuicPort); jsonWriter.WritePropertyName("reverseProxyNetworkACL"); { jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.ReverseProxyNetworkACL is not null) { foreach (NetworkAccessControl nac in _dnsWebService._dnsServer.ReverseProxyNetworkACL) jsonWriter.WriteStringValue(nac.ToString()); } jsonWriter.WriteEndArray(); } jsonWriter.WriteString("dnsTlsCertificatePath", _dnsWebService._dnsServer.DnsTlsCertificatePath); jsonWriter.WriteString("dnsTlsCertificatePassword", "************"); jsonWriter.WriteString("dnsOverHttpRealIpHeader", _dnsWebService._dnsServer.DnsOverHttpRealIpHeader); //tsig jsonWriter.WritePropertyName("tsigKeys"); { jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.TsigKeys is not null) { foreach (KeyValuePair tsigKey in _dnsWebService._dnsServer.TsigKeys.ToImmutableSortedDictionary()) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("keyName", tsigKey.Key); jsonWriter.WriteString("sharedSecret", tsigKey.Value.SharedSecret); jsonWriter.WriteString("algorithmName", tsigKey.Value.AlgorithmName); jsonWriter.WriteEndObject(); } } jsonWriter.WriteEndArray(); } //recursion jsonWriter.WriteString("recursion", _dnsWebService._dnsServer.Recursion.ToString()); jsonWriter.WritePropertyName("recursionNetworkACL"); { jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.RecursionNetworkACL is not null) { foreach (NetworkAccessControl nac in _dnsWebService._dnsServer.RecursionNetworkACL) jsonWriter.WriteStringValue(nac.ToString()); } jsonWriter.WriteEndArray(); } jsonWriter.WriteBoolean("randomizeName", _dnsWebService._dnsServer.RandomizeName); jsonWriter.WriteBoolean("qnameMinimization", _dnsWebService._dnsServer.QnameMinimization); jsonWriter.WriteNumber("resolverRetries", _dnsWebService._dnsServer.ResolverRetries); jsonWriter.WriteNumber("resolverTimeout", _dnsWebService._dnsServer.ResolverTimeout); jsonWriter.WriteNumber("resolverConcurrency", _dnsWebService._dnsServer.ResolverConcurrency); jsonWriter.WriteNumber("resolverMaxStackCount", _dnsWebService._dnsServer.ResolverMaxStackCount); //cache jsonWriter.WriteBoolean("saveCache", _dnsWebService._dnsServer.SaveCacheToDisk); jsonWriter.WriteBoolean("serveStale", _dnsWebService._dnsServer.ServeStale); jsonWriter.WriteNumber("serveStaleTtl", _dnsWebService._dnsServer.CacheZoneManager.ServeStaleTtl); jsonWriter.WriteNumber("serveStaleAnswerTtl", _dnsWebService._dnsServer.CacheZoneManager.ServeStaleAnswerTtl); jsonWriter.WriteNumber("serveStaleResetTtl", _dnsWebService._dnsServer.CacheZoneManager.ServeStaleResetTtl); jsonWriter.WriteNumber("serveStaleMaxWaitTime", _dnsWebService._dnsServer.ServeStaleMaxWaitTime); jsonWriter.WriteNumber("cacheMaximumEntries", _dnsWebService._dnsServer.CacheZoneManager.MaximumEntries); jsonWriter.WriteNumber("cacheMinimumRecordTtl", _dnsWebService._dnsServer.CacheZoneManager.MinimumRecordTtl); jsonWriter.WriteNumber("cacheMaximumRecordTtl", _dnsWebService._dnsServer.CacheZoneManager.MaximumRecordTtl); jsonWriter.WriteNumber("cacheNegativeRecordTtl", _dnsWebService._dnsServer.CacheZoneManager.NegativeRecordTtl); jsonWriter.WriteNumber("cacheFailureRecordTtl", _dnsWebService._dnsServer.CacheZoneManager.FailureRecordTtl); jsonWriter.WriteNumber("cachePrefetchEligibility", _dnsWebService._dnsServer.CachePrefetchEligibility); jsonWriter.WriteNumber("cachePrefetchTrigger", _dnsWebService._dnsServer.CachePrefetchTrigger); jsonWriter.WriteNumber("cachePrefetchSampleIntervalInMinutes", _dnsWebService._dnsServer.CachePrefetchSampleIntervalMinutes); jsonWriter.WriteNumber("cachePrefetchSampleEligibilityHitsPerHour", _dnsWebService._dnsServer.CachePrefetchSampleEligibilityHitsPerHour); //blocking jsonWriter.WriteBoolean("enableBlocking", _dnsWebService._dnsServer.EnableBlocking); jsonWriter.WriteBoolean("allowTxtBlockingReport", _dnsWebService._dnsServer.AllowTxtBlockingReport); jsonWriter.WritePropertyName("blockingBypassList"); jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.BlockingBypassList is not null) { foreach (NetworkAddress network in _dnsWebService._dnsServer.BlockingBypassList) jsonWriter.WriteStringValue(network.ToString()); } jsonWriter.WriteEndArray(); if (!_dnsWebService._dnsServer.EnableBlocking && (DateTime.UtcNow < _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlockingTill)) jsonWriter.WriteString("temporaryDisableBlockingTill", _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlockingTill); jsonWriter.WriteString("blockingType", _dnsWebService._dnsServer.BlockingType.ToString()); jsonWriter.WriteNumber("blockingAnswerTtl", _dnsWebService._dnsServer.BlockingAnswerTtl); jsonWriter.WritePropertyName("customBlockingAddresses"); jsonWriter.WriteStartArray(); foreach (DnsARecordData record in _dnsWebService._dnsServer.CustomBlockingARecords) jsonWriter.WriteStringValue(record.Address.ToString()); foreach (DnsAAAARecordData record in _dnsWebService._dnsServer.CustomBlockingAAAARecords) jsonWriter.WriteStringValue(record.Address.ToString()); jsonWriter.WriteEndArray(); jsonWriter.WritePropertyName("blockListUrls"); if (_dnsWebService._dnsServer.BlockListZoneManager.BlockListUrls.Count == 0) { jsonWriter.WriteNullValue(); } else { jsonWriter.WriteStartArray(); foreach (string blockListUrl in _dnsWebService._dnsServer.BlockListZoneManager.BlockListUrls) jsonWriter.WriteStringValue(blockListUrl); jsonWriter.WriteEndArray(); } jsonWriter.WriteNumber("blockListUpdateIntervalHours", _dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours); if (_dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateEnabled) { DateTime blockListNextUpdatedOn = _dnsWebService._dnsServer.BlockListZoneManager.BlockListLastUpdatedOn.AddHours(_dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours); jsonWriter.WriteString("blockListNextUpdatedOn", blockListNextUpdatedOn); } //proxy & forwarders jsonWriter.WritePropertyName("proxy"); if (_dnsWebService._dnsServer.Proxy == null) { jsonWriter.WriteNullValue(); } else { jsonWriter.WriteStartObject(); NetProxy proxy = _dnsWebService._dnsServer.Proxy; jsonWriter.WriteString("type", proxy.Type.ToString()); jsonWriter.WriteString("address", proxy.Address); jsonWriter.WriteNumber("port", proxy.Port); NetworkCredential credential = proxy.Credential; if (credential != null) { jsonWriter.WriteString("username", credential.UserName); jsonWriter.WriteString("password", credential.Password); } jsonWriter.WritePropertyName("bypass"); jsonWriter.WriteStartArray(); foreach (NetProxyBypassItem item in proxy.BypassList) jsonWriter.WriteStringValue(item.Value); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); } jsonWriter.WritePropertyName("forwarders"); DnsTransportProtocol forwarderProtocol = DnsTransportProtocol.Udp; if (_dnsWebService._dnsServer.Forwarders == null) { jsonWriter.WriteNullValue(); } else { forwarderProtocol = _dnsWebService._dnsServer.Forwarders[0].Protocol; jsonWriter.WriteStartArray(); foreach (NameServerAddress forwarder in _dnsWebService._dnsServer.Forwarders) jsonWriter.WriteStringValue(forwarder.OriginalAddress); jsonWriter.WriteEndArray(); } jsonWriter.WriteString("forwarderProtocol", forwarderProtocol.ToString()); jsonWriter.WriteBoolean("concurrentForwarding", _dnsWebService._dnsServer.ConcurrentForwarding); jsonWriter.WriteNumber("forwarderRetries", _dnsWebService._dnsServer.ForwarderRetries); jsonWriter.WriteNumber("forwarderTimeout", _dnsWebService._dnsServer.ForwarderTimeout); jsonWriter.WriteNumber("forwarderConcurrency", _dnsWebService._dnsServer.ForwarderConcurrency); //logging jsonWriter.WriteBoolean("enableLogging", _dnsWebService._log.LoggingType != LoggingType.None); jsonWriter.WriteString("loggingType", _dnsWebService._log.LoggingType.ToString()); jsonWriter.WriteBoolean("ignoreResolverLogs", _dnsWebService._dnsServer.ResolverLogManager == null); jsonWriter.WriteBoolean("logQueries", _dnsWebService._dnsServer.QueryLogManager != null); jsonWriter.WriteBoolean("useLocalTime", _dnsWebService._log.UseLocalTime); jsonWriter.WriteString("logFolder", _dnsWebService._log.LogFolder); jsonWriter.WriteNumber("maxLogFileDays", _dnsWebService._log.MaxLogFileDays); jsonWriter.WriteBoolean("enableInMemoryStats", _dnsWebService._dnsServer.StatsManager.EnableInMemoryStats); jsonWriter.WriteNumber("maxStatFileDays", _dnsWebService._dnsServer.StatsManager.MaxStatFileDays); } #endregion #region public public void GetDnsSettings(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteDnsSettings(jsonWriter); } public async Task SetDnsSettingsAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); bool serverDomainChanged = false; bool webServiceLocalAddressesChanged = false; bool webServiceTlsCertificateChanged = false; bool restartDnsService = false; bool restartWebService = false; IReadOnlyList oldWebServiceLocalAddresses = _dnsWebService._webServiceLocalAddresses; int oldWebServiceHttpPort = _dnsWebService._webServiceHttpPort; int oldWebServiceTlsPort = _dnsWebService._webServiceTlsPort; bool _webServiceEnablingTls = false; Dictionary clusterParameters = new Dictionary(128); HttpRequest request = context.Request; JsonDocument jsonDocument = null; if (request.HasJsonContentType()) { jsonDocument = await JsonDocument.ParseAsync(request.Body); context.Items["jsonContent"] = jsonDocument; } try { try { #region general if (request.TryGetQueryOrForm("dnsServerDomain", out string dnsServerDomain)) { dnsServerDomain = dnsServerDomain.TrimEnd('.'); if (!_dnsWebService._dnsServer.ServerDomain.Equals(dnsServerDomain, StringComparison.OrdinalIgnoreCase)) { if (_dnsWebService._clusterManager.ClusterInitialized) { if (!dnsServerDomain.EndsWith("." + _dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("DNS server domain name must end with the cluster domain name.", nameof(dnsServerDomain)); } _dnsWebService._dnsServer.ServerDomain = dnsServerDomain; serverDomainChanged = true; } } if (request.TryGetQueryOrFormArray("dnsServerLocalEndPoints", IPEndPoint.Parse, out IPEndPoint[] dnsServerLocalEndPoints)) { if (dnsServerLocalEndPoints.Length == 0) { dnsServerLocalEndPoints = [new IPEndPoint(IPAddress.Any, 53), new IPEndPoint(IPAddress.IPv6Any, 53)]; } else { foreach (IPEndPoint localEndPoint in dnsServerLocalEndPoints) { if (localEndPoint.Port == 0) localEndPoint.Port = 53; } } if (!_dnsWebService._dnsServer.LocalEndPoints.HasSameItems(dnsServerLocalEndPoints)) restartDnsService = true; _dnsWebService._dnsServer.LocalEndPoints = dnsServerLocalEndPoints; } if (request.TryGetQueryOrFormArray("dnsServerIPv4SourceAddresses", NetworkAddress.Parse, out NetworkAddress[] dnsServerIPv4SourceAddresses)) DnsClientConnection.IPv4SourceAddresses = dnsServerIPv4SourceAddresses; if (request.TryGetQueryOrFormArray("dnsServerIPv6SourceAddresses", NetworkAddress.Parse, out NetworkAddress[] dnsServerIPv6SourceAddresses)) DnsClientConnection.IPv6SourceAddresses = dnsServerIPv6SourceAddresses; if (request.TryGetQueryOrForm("defaultRecordTtl", ZoneFile.ParseTtl, out uint defaultRecordTtl)) { _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl = defaultRecordTtl; clusterParameters.Add("defaultRecordTtl", defaultRecordTtl.ToString()); } if (request.TryGetQueryOrForm("defaultNsRecordTtl", ZoneFile.ParseTtl, out uint defaultNsRecordTtl)) { _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl = defaultNsRecordTtl; clusterParameters.Add("defaultNsRecordTtl", defaultNsRecordTtl.ToString()); } if (request.TryGetQueryOrForm("defaultSoaRecordTtl", ZoneFile.ParseTtl, out uint defaultSoaRecordTtl)) { _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl = defaultSoaRecordTtl; clusterParameters.Add("defaultSoaRecordTtl", defaultSoaRecordTtl.ToString()); } string defaultResponsiblePerson = request.QueryOrForm("defaultResponsiblePerson"); if (defaultResponsiblePerson is not null) { if (defaultResponsiblePerson.Length == 0) _dnsWebService._dnsServer.DefaultResponsiblePerson = null; else if (defaultResponsiblePerson.Length > 255) throw new ArgumentException("Default responsible person email address length cannot exceed 255 characters.", nameof(defaultResponsiblePerson)); else _dnsWebService._dnsServer.DefaultResponsiblePerson = new MailAddress(defaultResponsiblePerson); clusterParameters.Add("defaultResponsiblePerson", defaultResponsiblePerson); } if (request.TryGetQueryOrForm("useSoaSerialDateScheme", bool.Parse, out bool useSoaSerialDateScheme)) { _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme = useSoaSerialDateScheme; clusterParameters.Add("useSoaSerialDateScheme", useSoaSerialDateScheme.ToString()); } if (request.TryGetQueryOrForm("minSoaRefresh", ZoneFile.ParseTtl, out uint minSoaRefresh)) { _dnsWebService._dnsServer.AuthZoneManager.MinSoaRefresh = minSoaRefresh; clusterParameters.Add("minSoaRefresh", minSoaRefresh.ToString()); } if (request.TryGetQueryOrForm("minSoaRetry", ZoneFile.ParseTtl, out uint minSoaRetry)) { _dnsWebService._dnsServer.AuthZoneManager.MinSoaRetry = minSoaRetry; clusterParameters.Add("minSoaRetry", minSoaRetry.ToString()); } if (request.TryGetQueryOrFormArray("zoneTransferAllowedNetworks", NetworkAddress.Parse, out NetworkAddress[] zoneTransferAllowedNetworks)) { _dnsWebService._dnsServer.ZoneTransferAllowedNetworks = zoneTransferAllowedNetworks; clusterParameters.Add("zoneTransferAllowedNetworks", zoneTransferAllowedNetworks.Join()); } if (request.TryGetQueryOrFormArray("notifyAllowedNetworks", NetworkAddress.Parse, out NetworkAddress[] notifyAllowedNetworks)) { _dnsWebService._dnsServer.NotifyAllowedNetworks = notifyAllowedNetworks; clusterParameters.Add("notifyAllowedNetworks", notifyAllowedNetworks.Join()); } if (request.TryGetQueryOrForm("dnsAppsEnableAutomaticUpdate", bool.Parse, out bool dnsAppsEnableAutomaticUpdate)) { _dnsWebService._dnsServer.DnsApplicationManager.EnableAutomaticUpdate = dnsAppsEnableAutomaticUpdate; clusterParameters.Add("dnsAppsEnableAutomaticUpdate", dnsAppsEnableAutomaticUpdate.ToString()); } if (request.TryGetQueryOrForm("preferIPv6", bool.Parse, out bool preferIPv6)) _dnsWebService._dnsServer.PreferIPv6 = preferIPv6; if (request.TryGetQueryOrForm("enableUdpSocketPool", bool.Parse, out bool enableUdpSocketPool)) _dnsWebService._dnsServer.EnableUdpSocketPool = enableUdpSocketPool; if (request.TryGetQueryOrFormArray("socketPoolExcludedPorts", ushort.Parse, out ushort[] socketPoolExcludedPorts)) UdpClientConnection.SocketPoolExcludedPorts = socketPoolExcludedPorts; if (request.TryGetQueryOrForm("udpPayloadSize", ushort.Parse, out ushort udpPayloadSize)) { _dnsWebService._dnsServer.UdpPayloadSize = udpPayloadSize; clusterParameters.Add("udpPayloadSize", udpPayloadSize.ToString()); } if (request.TryGetQueryOrForm("dnssecValidation", bool.Parse, out bool dnssecValidation)) { _dnsWebService._dnsServer.DnssecValidation = dnssecValidation; clusterParameters.Add("dnssecValidation", dnssecValidation.ToString()); } if (request.TryGetQueryOrForm("eDnsClientSubnet", bool.Parse, out bool eDnsClientSubnet)) { _dnsWebService._dnsServer.EDnsClientSubnet = eDnsClientSubnet; clusterParameters.Add("eDnsClientSubnet", eDnsClientSubnet.ToString()); } if (request.TryGetQueryOrForm("eDnsClientSubnetIPv4PrefixLength", byte.Parse, out byte eDnsClientSubnetIPv4PrefixLength)) { _dnsWebService._dnsServer.EDnsClientSubnetIPv4PrefixLength = eDnsClientSubnetIPv4PrefixLength; clusterParameters.Add("eDnsClientSubnetIPv4PrefixLength", eDnsClientSubnetIPv4PrefixLength.ToString()); } if (request.TryGetQueryOrForm("eDnsClientSubnetIPv6PrefixLength", byte.Parse, out byte eDnsClientSubnetIPv6PrefixLength)) { _dnsWebService._dnsServer.EDnsClientSubnetIPv6PrefixLength = eDnsClientSubnetIPv6PrefixLength; clusterParameters.Add("eDnsClientSubnetIPv6PrefixLength", eDnsClientSubnetIPv6PrefixLength.ToString()); } string eDnsClientSubnetIpv4Override = request.QueryOrForm("eDnsClientSubnetIpv4Override"); if (eDnsClientSubnetIpv4Override is not null) { if (eDnsClientSubnetIpv4Override.Length == 0) _dnsWebService._dnsServer.EDnsClientSubnetIpv4Override = null; else _dnsWebService._dnsServer.EDnsClientSubnetIpv4Override = NetworkAddress.Parse(eDnsClientSubnetIpv4Override); clusterParameters.Add("eDnsClientSubnetIpv4Override", eDnsClientSubnetIpv4Override); } string eDnsClientSubnetIpv6Override = request.QueryOrForm("eDnsClientSubnetIpv6Override"); if (eDnsClientSubnetIpv6Override is not null) { if (eDnsClientSubnetIpv6Override.Length == 0) _dnsWebService._dnsServer.EDnsClientSubnetIpv6Override = null; else _dnsWebService._dnsServer.EDnsClientSubnetIpv6Override = NetworkAddress.Parse(eDnsClientSubnetIpv6Override); clusterParameters.Add("eDnsClientSubnetIpv6Override", eDnsClientSubnetIpv6Override); } if (request.TryGetQueryOrFormArray("qpmPrefixLimitsIPv4", delegate (JsonElement jsonObject) { int prefix = jsonObject.GetProperty("prefix").GetInt32(); int udpLimit = jsonObject.GetProperty("udpLimit").GetInt32(); int tcpLimit = jsonObject.GetProperty("tcpLimit").GetInt32(); return new KeyValuePair(prefix, (udpLimit, tcpLimit)); }, delegate (ArraySegment tableRow) { int prefix = int.Parse(tableRow[0]); int udpLimit = int.Parse(tableRow[1]); int tcpLimit = int.Parse(tableRow[2]); return new KeyValuePair(prefix, (udpLimit, tcpLimit)); }, 3, out KeyValuePair[] qpmPrefixLimitsIPv4, '|')) { string strQpmPrefixLimitsIPv4 = ""; if (qpmPrefixLimitsIPv4.Length == 0) { _dnsWebService._dnsServer.QpmPrefixLimitsIPv4 = null; } else { Dictionary qpmPrefixLimitsIPv4Map = new Dictionary(qpmPrefixLimitsIPv4.Length); foreach (KeyValuePair qpmPrefixLimit in qpmPrefixLimitsIPv4) { qpmPrefixLimitsIPv4Map.Add(qpmPrefixLimit.Key, qpmPrefixLimit.Value); if (strQpmPrefixLimitsIPv4.Length == 0) strQpmPrefixLimitsIPv4 = qpmPrefixLimit.Key + "|" + qpmPrefixLimit.Value.Item1 + "|" + qpmPrefixLimit.Value.Item2; else strQpmPrefixLimitsIPv4 += "|" + qpmPrefixLimit.Key + "|" + qpmPrefixLimit.Value.Item1 + "|" + qpmPrefixLimit.Value.Item2; } _dnsWebService._dnsServer.QpmPrefixLimitsIPv4 = qpmPrefixLimitsIPv4Map; } clusterParameters.Add("qpmPrefixLimitsIPv4", strQpmPrefixLimitsIPv4); } if (request.TryGetQueryOrFormArray("qpmPrefixLimitsIPv6", delegate (JsonElement jsonObject) { int prefix = jsonObject.GetProperty("prefix").GetInt32(); int udpLimit = jsonObject.GetProperty("udpLimit").GetInt32(); int tcpLimit = jsonObject.GetProperty("tcpLimit").GetInt32(); return new KeyValuePair(prefix, (udpLimit, tcpLimit)); }, delegate (ArraySegment tableRow) { int prefix = int.Parse(tableRow[0]); int udpLimit = int.Parse(tableRow[1]); int tcpLimit = int.Parse(tableRow[2]); return new KeyValuePair(prefix, (udpLimit, tcpLimit)); }, 3, out KeyValuePair[] qpmPrefixLimitsIPv6, '|')) { string strQpmPrefixLimitsIPv6 = ""; if (qpmPrefixLimitsIPv6.Length == 0) { _dnsWebService._dnsServer.QpmPrefixLimitsIPv6 = null; } else { Dictionary qpmPrefixLimitsIPv6Map = new Dictionary(qpmPrefixLimitsIPv6.Length); foreach (KeyValuePair qpmPrefixLimit in qpmPrefixLimitsIPv6) { qpmPrefixLimitsIPv6Map.Add(qpmPrefixLimit.Key, qpmPrefixLimit.Value); if (strQpmPrefixLimitsIPv6.Length == 0) strQpmPrefixLimitsIPv6 = qpmPrefixLimit.Key + "|" + qpmPrefixLimit.Value.Item1 + "|" + qpmPrefixLimit.Value.Item2; else strQpmPrefixLimitsIPv6 += "|" + qpmPrefixLimit.Key + "|" + qpmPrefixLimit.Value.Item1 + "|" + qpmPrefixLimit.Value.Item2; } _dnsWebService._dnsServer.QpmPrefixLimitsIPv6 = qpmPrefixLimitsIPv6Map; } clusterParameters.Add("qpmPrefixLimitsIPv6", strQpmPrefixLimitsIPv6); } if (request.TryGetQueryOrForm("qpmLimitSampleMinutes", int.Parse, out int qpmLimitSampleMinutes)) { _dnsWebService._dnsServer.QpmLimitSampleMinutes = qpmLimitSampleMinutes; clusterParameters.Add("qpmLimitSampleMinutes", qpmLimitSampleMinutes.ToString()); } if (request.TryGetQueryOrForm("qpmLimitUdpTruncationPercentage", int.Parse, out int qpmLimitUdpTruncationPercentage)) { _dnsWebService._dnsServer.QpmLimitUdpTruncationPercentage = qpmLimitUdpTruncationPercentage; clusterParameters.Add("qpmLimitUdpTruncationPercentage", qpmLimitUdpTruncationPercentage.ToString()); } if (request.TryGetQueryOrFormArray("qpmLimitBypassList", NetworkAddress.Parse, out NetworkAddress[] qpmLimitBypassList)) { _dnsWebService._dnsServer.QpmLimitBypassList = qpmLimitBypassList; clusterParameters.Add("qpmLimitBypassList", qpmLimitBypassList.Join()); } if (request.TryGetQueryOrForm("clientTimeout", int.Parse, out int clientTimeout)) { _dnsWebService._dnsServer.ClientTimeout = clientTimeout; clusterParameters.Add("clientTimeout", clientTimeout.ToString()); } if (request.TryGetQueryOrForm("tcpSendTimeout", int.Parse, out int tcpSendTimeout)) { if (_dnsWebService._dnsServer.TcpSendTimeout != tcpSendTimeout) { _dnsWebService._dnsServer.TcpSendTimeout = tcpSendTimeout; restartDnsService = true; } clusterParameters.Add("tcpSendTimeout", tcpSendTimeout.ToString()); } if (request.TryGetQueryOrForm("tcpReceiveTimeout", int.Parse, out int tcpReceiveTimeout)) { if (_dnsWebService._dnsServer.TcpReceiveTimeout != tcpReceiveTimeout) { _dnsWebService._dnsServer.TcpReceiveTimeout = tcpReceiveTimeout; restartDnsService = true; } clusterParameters.Add("tcpReceiveTimeout", tcpReceiveTimeout.ToString()); } if (request.TryGetQueryOrForm("quicIdleTimeout", int.Parse, out int quicIdleTimeout)) { _dnsWebService._dnsServer.QuicIdleTimeout = quicIdleTimeout; clusterParameters.Add("quicIdleTimeout", quicIdleTimeout.ToString()); } if (request.TryGetQueryOrForm("quicMaxInboundStreams", int.Parse, out int quicMaxInboundStreams)) { _dnsWebService._dnsServer.QuicMaxInboundStreams = quicMaxInboundStreams; clusterParameters.Add("quicMaxInboundStreams", quicMaxInboundStreams.ToString()); } if (request.TryGetQueryOrForm("listenBacklog", int.Parse, out int listenBacklog)) { if (_dnsWebService._dnsServer.ListenBacklog != listenBacklog) { _dnsWebService._dnsServer.ListenBacklog = listenBacklog; restartDnsService = true; } clusterParameters.Add("listenBacklog", listenBacklog.ToString()); } if (request.TryGetQueryOrForm("maxConcurrentResolutionsPerCore", ushort.Parse, out ushort maxConcurrentResolutionsPerCore)) { _dnsWebService._dnsServer.MaxConcurrentResolutionsPerCore = maxConcurrentResolutionsPerCore; clusterParameters.Add("maxConcurrentResolutionsPerCore", maxConcurrentResolutionsPerCore.ToString()); } #endregion #region web service if (request.TryGetQueryOrFormArray("webServiceLocalAddresses", IPAddress.Parse, out IPAddress[] webServiceLocalAddresses)) { if (webServiceLocalAddresses.Length == 0) webServiceLocalAddresses = [IPAddress.Any, IPAddress.IPv6Any]; if (!_dnsWebService._webServiceLocalAddresses.HasSameItems(webServiceLocalAddresses)) { webServiceLocalAddressesChanged = true; restartWebService = true; } _dnsWebService._webServiceLocalAddresses = WebUtilities.GetValidKestrelLocalAddresses(webServiceLocalAddresses); } if (request.TryGetQueryOrForm("webServiceHttpPort", int.Parse, out int webServiceHttpPort)) { if (_dnsWebService._webServiceHttpPort != webServiceHttpPort) { _dnsWebService._webServiceHttpPort = webServiceHttpPort; restartWebService = true; } } if (request.TryGetQueryOrForm("webServiceEnableTls", bool.Parse, out bool webServiceEnableTls)) { if (_dnsWebService._webServiceEnableTls != webServiceEnableTls) { _dnsWebService._webServiceEnableTls = webServiceEnableTls; _webServiceEnablingTls = webServiceEnableTls; restartWebService = true; } } if (request.TryGetQueryOrForm("webServiceEnableHttp3", bool.Parse, out bool webServiceEnableHttp3)) { if (_dnsWebService._webServiceEnableHttp3 != webServiceEnableHttp3) { if (webServiceEnableHttp3) DnsWebService.ValidateQuicSupport("HTTP/3"); _dnsWebService._webServiceEnableHttp3 = webServiceEnableHttp3; restartWebService = true; } } if (request.TryGetQueryOrForm("webServiceHttpToTlsRedirect", bool.Parse, out bool webServiceHttpToTlsRedirect)) { if (_dnsWebService._webServiceHttpToTlsRedirect != webServiceHttpToTlsRedirect) { _dnsWebService._webServiceHttpToTlsRedirect = webServiceHttpToTlsRedirect; restartWebService = true; } } if (request.TryGetQueryOrForm("webServiceUseSelfSignedTlsCertificate", bool.Parse, out bool webServiceUseSelfSignedTlsCertificate)) _dnsWebService._webServiceUseSelfSignedTlsCertificate = webServiceUseSelfSignedTlsCertificate; if (request.TryGetQueryOrForm("webServiceTlsPort", int.Parse, out int webServiceTlsPort)) { if (_dnsWebService._webServiceTlsPort != webServiceTlsPort) { _dnsWebService._webServiceTlsPort = webServiceTlsPort; restartWebService = true; } } string webServiceTlsCertificatePath = request.QueryOrForm("webServiceTlsCertificatePath"); if (webServiceTlsCertificatePath is not null) { if (webServiceTlsCertificatePath.Length == 0) { if (!string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath)) { _dnsWebService.RemoveWebServiceTlsCertificate(); webServiceTlsCertificateChanged = true; } } else { string webServiceTlsCertificatePassword = request.QueryOrForm("webServiceTlsCertificatePassword"); if ((webServiceTlsCertificatePassword is null) || (webServiceTlsCertificatePassword == "************")) webServiceTlsCertificatePassword = _dnsWebService._webServiceTlsCertificatePassword; if ((webServiceTlsCertificatePath != _dnsWebService._webServiceTlsCertificatePath) || (webServiceTlsCertificatePassword != _dnsWebService._webServiceTlsCertificatePassword)) { _dnsWebService.SetWebServiceTlsCertificate(webServiceTlsCertificatePath, webServiceTlsCertificatePassword); webServiceTlsCertificateChanged = true; } } } if (request.TryGetQueryOrForm("webServiceRealIpHeader", out string webServiceRealIpHeader)) { if (webServiceRealIpHeader.Length > 255) throw new ArgumentException("Web service Real IP header name cannot exceed 255 characters.", nameof(webServiceRealIpHeader)); if (webServiceRealIpHeader.Contains(' ')) throw new ArgumentException("Web service Real IP header name cannot contain invalid characters.", nameof(webServiceRealIpHeader)); _dnsWebService._webServiceRealIpHeader = webServiceRealIpHeader; } #endregion #region optional protocols if (request.TryGetQueryOrForm("enableDnsOverUdpProxy", bool.Parse, out bool enableDnsOverUdpProxy)) { if (_dnsWebService._dnsServer.EnableDnsOverUdpProxy != enableDnsOverUdpProxy) { _dnsWebService._dnsServer.EnableDnsOverUdpProxy = enableDnsOverUdpProxy; restartDnsService = true; } } if (request.TryGetQueryOrForm("enableDnsOverTcpProxy", bool.Parse, out bool enableDnsOverTcpProxy)) { if (_dnsWebService._dnsServer.EnableDnsOverTcpProxy != enableDnsOverTcpProxy) { _dnsWebService._dnsServer.EnableDnsOverTcpProxy = enableDnsOverTcpProxy; restartDnsService = true; } } if (request.TryGetQueryOrForm("enableDnsOverHttp", bool.Parse, out bool enableDnsOverHttp)) { if (_dnsWebService._dnsServer.EnableDnsOverHttp != enableDnsOverHttp) { _dnsWebService._dnsServer.EnableDnsOverHttp = enableDnsOverHttp; restartDnsService = true; } } if (request.TryGetQueryOrForm("enableDnsOverTls", bool.Parse, out bool enableDnsOverTls)) { if (_dnsWebService._dnsServer.EnableDnsOverTls != enableDnsOverTls) { _dnsWebService._dnsServer.EnableDnsOverTls = enableDnsOverTls; restartDnsService = true; } } if (request.TryGetQueryOrForm("enableDnsOverHttps", bool.Parse, out bool enableDnsOverHttps)) { if (_dnsWebService._dnsServer.EnableDnsOverHttps != enableDnsOverHttps) { _dnsWebService._dnsServer.EnableDnsOverHttps = enableDnsOverHttps; restartDnsService = true; } } if (request.TryGetQueryOrForm("enableDnsOverHttp3", bool.Parse, out bool enableDnsOverHttp3)) { if (_dnsWebService._dnsServer.EnableDnsOverHttp3 != enableDnsOverHttp3) { if (enableDnsOverHttp3) DnsWebService.ValidateQuicSupport("DNS-over-HTTP/3"); _dnsWebService._dnsServer.EnableDnsOverHttp3 = enableDnsOverHttp3; restartDnsService = true; } } if (request.TryGetQueryOrForm("enableDnsOverQuic", bool.Parse, out bool enableDnsOverQuic)) { if (_dnsWebService._dnsServer.EnableDnsOverQuic != enableDnsOverQuic) { if (enableDnsOverQuic) DnsWebService.ValidateQuicSupport(); _dnsWebService._dnsServer.EnableDnsOverQuic = enableDnsOverQuic; restartDnsService = true; } } if (request.TryGetQueryOrForm("dnsOverUdpProxyPort", int.Parse, out int dnsOverUdpProxyPort)) { if (_dnsWebService._dnsServer.DnsOverUdpProxyPort != dnsOverUdpProxyPort) { _dnsWebService._dnsServer.DnsOverUdpProxyPort = dnsOverUdpProxyPort; restartDnsService = true; } } if (request.TryGetQueryOrForm("dnsOverTcpProxyPort", int.Parse, out int dnsOverTcpProxyPort)) { if (_dnsWebService._dnsServer.DnsOverTcpProxyPort != dnsOverTcpProxyPort) { _dnsWebService._dnsServer.DnsOverTcpProxyPort = dnsOverTcpProxyPort; restartDnsService = true; } } if (request.TryGetQueryOrForm("dnsOverHttpPort", int.Parse, out int dnsOverHttpPort)) { if (_dnsWebService._dnsServer.DnsOverHttpPort != dnsOverHttpPort) { _dnsWebService._dnsServer.DnsOverHttpPort = dnsOverHttpPort; restartDnsService = true; } } if (request.TryGetQueryOrForm("dnsOverTlsPort", int.Parse, out int dnsOverTlsPort)) { if (_dnsWebService._dnsServer.DnsOverTlsPort != dnsOverTlsPort) { _dnsWebService._dnsServer.DnsOverTlsPort = dnsOverTlsPort; restartDnsService = true; } } if (request.TryGetQueryOrForm("dnsOverHttpsPort", int.Parse, out int dnsOverHttpsPort)) { if (_dnsWebService._dnsServer.DnsOverHttpsPort != dnsOverHttpsPort) { _dnsWebService._dnsServer.DnsOverHttpsPort = dnsOverHttpsPort; restartDnsService = true; } } if (request.TryGetQueryOrForm("dnsOverQuicPort", int.Parse, out int dnsOverQuicPort)) { if (_dnsWebService._dnsServer.DnsOverQuicPort != dnsOverQuicPort) { _dnsWebService._dnsServer.DnsOverQuicPort = dnsOverQuicPort; restartDnsService = true; } } if (request.TryGetQueryOrFormArray("reverseProxyNetworkACL", NetworkAccessControl.Parse, out NetworkAccessControl[] reverseProxyNetworkACL)) _dnsWebService._dnsServer.ReverseProxyNetworkACL = reverseProxyNetworkACL; string dnsTlsCertificatePath = request.QueryOrForm("dnsTlsCertificatePath"); if (dnsTlsCertificatePath is not null) { if (dnsTlsCertificatePath.Length == 0) { if (!string.IsNullOrEmpty(_dnsWebService._dnsServer.DnsTlsCertificatePath) && (_dnsWebService._dnsServer.EnableDnsOverTls || _dnsWebService._dnsServer.EnableDnsOverHttps || _dnsWebService._dnsServer.EnableDnsOverQuic)) restartDnsService = true; _dnsWebService._dnsServer.RemoveDnsTlsCertificate(); } else { string dnsTlsCertificatePassword = request.QueryOrForm("dnsTlsCertificatePassword"); if ((dnsTlsCertificatePassword is null) || (dnsTlsCertificatePassword == "************")) dnsTlsCertificatePassword = _dnsWebService._dnsServer.DnsTlsCertificatePassword; if ((dnsTlsCertificatePath != _dnsWebService._dnsServer.DnsTlsCertificatePath) || (dnsTlsCertificatePassword != _dnsWebService._dnsServer.DnsTlsCertificatePassword)) { _dnsWebService._dnsServer.SetDnsTlsCertificate(dnsTlsCertificatePath, dnsTlsCertificatePassword); if (string.IsNullOrEmpty(_dnsWebService._dnsServer.DnsTlsCertificatePath) && (_dnsWebService._dnsServer.EnableDnsOverTls || _dnsWebService._dnsServer.EnableDnsOverHttps || _dnsWebService._dnsServer.EnableDnsOverQuic)) restartDnsService = true; } } } if (request.TryGetQueryOrForm("dnsOverHttpRealIpHeader", out string dnsOverHttpRealIpHeader)) _dnsWebService._dnsServer.DnsOverHttpRealIpHeader = dnsOverHttpRealIpHeader; #endregion #region tsig if (request.TryGetQueryOrFormArray("tsigKeys", delegate (JsonElement jsonObject) { string keyName = jsonObject.GetProperty("keyName").GetString().TrimEnd('.').ToLowerInvariant(); string sharedSecret = jsonObject.GetProperty("sharedSecret").GetString(); string algorithmName = jsonObject.GetProperty("algorithmName").GetString(); if (DnsClient.IsDomainNameUnicode(keyName)) keyName = DnsClient.ConvertDomainNameToAscii(keyName); DnsClient.IsDomainNameValid(keyName, true); if (sharedSecret.Length == 0) return new TsigKey(keyName, algorithmName); return new TsigKey(keyName, sharedSecret, algorithmName); }, delegate (ArraySegment tableRow) { string keyName = tableRow[0].TrimEnd('.').ToLowerInvariant(); string sharedSecret = tableRow[1]; string algorithmName = tableRow[2]; if (DnsClient.IsDomainNameUnicode(keyName)) keyName = DnsClient.ConvertDomainNameToAscii(keyName); DnsClient.IsDomainNameValid(keyName, true); if (sharedSecret.Length == 0) return new TsigKey(keyName, algorithmName); return new TsigKey(keyName, sharedSecret, algorithmName); }, 3, out TsigKey[] tsigKeys, '|') ) { string strTsigKeys = ""; if (tsigKeys.Length == 0) { if (_dnsWebService._clusterManager.ClusterInitialized) throw new DnsWebServiceException($"Cannot remove TSIG key for 'cluster-catalog.{_dnsWebService._clusterManager.ClusterDomain}' Cluster Catalog zone."); _dnsWebService._dnsServer.TsigKeys = null; } else { Dictionary tsigKeysMap = new Dictionary(tsigKeys.Length); foreach (TsigKey tsigKey in tsigKeys) { tsigKeysMap.Add(tsigKey.KeyName, tsigKey); if (strTsigKeys.Length == 0) strTsigKeys = tsigKey.KeyName + "|" + tsigKey.SharedSecret + "|" + tsigKey.AlgorithmName; else strTsigKeys += "|" + tsigKey.KeyName + "|" + tsigKey.SharedSecret + "|" + tsigKey.AlgorithmName; } if (_dnsWebService._clusterManager.ClusterInitialized) { if (!tsigKeysMap.ContainsKey($"cluster-catalog.{_dnsWebService._clusterManager.ClusterDomain}")) throw new DnsWebServiceException($"Cannot remove TSIG key for 'cluster-catalog.{_dnsWebService._clusterManager.ClusterDomain}' Cluster Catalog zone."); } _dnsWebService._dnsServer.TsigKeys = tsigKeysMap; } clusterParameters.Add("tsigKeys", strTsigKeys); } #endregion #region recursion if (request.TryGetQueryOrFormEnum("recursion", out DnsServerRecursion recursion)) { _dnsWebService._dnsServer.Recursion = recursion; clusterParameters.Add("recursion", recursion.ToString()); } if (request.TryGetQueryOrFormArray("recursionNetworkACL", NetworkAccessControl.Parse, out NetworkAccessControl[] recursionNetworkACL)) { _dnsWebService._dnsServer.RecursionNetworkACL = recursionNetworkACL; clusterParameters.Add("recursionNetworkACL", recursionNetworkACL.Join()); } if (request.TryGetQueryOrForm("randomizeName", bool.Parse, out bool randomizeName)) { _dnsWebService._dnsServer.RandomizeName = randomizeName; clusterParameters.Add("randomizeName", randomizeName.ToString()); } if (request.TryGetQueryOrForm("qnameMinimization", bool.Parse, out bool qnameMinimization)) { _dnsWebService._dnsServer.QnameMinimization = qnameMinimization; clusterParameters.Add("qnameMinimization", qnameMinimization.ToString()); } if (request.TryGetQueryOrForm("resolverRetries", int.Parse, out int resolverRetries)) { _dnsWebService._dnsServer.ResolverRetries = resolverRetries; clusterParameters.Add("resolverRetries", resolverRetries.ToString()); } if (request.TryGetQueryOrForm("resolverTimeout", int.Parse, out int resolverTimeout)) { _dnsWebService._dnsServer.ResolverTimeout = resolverTimeout; clusterParameters.Add("resolverTimeout", resolverTimeout.ToString()); } if (request.TryGetQueryOrForm("resolverConcurrency", int.Parse, out int resolverConcurrency)) { _dnsWebService._dnsServer.ResolverConcurrency = resolverConcurrency; clusterParameters.Add("resolverConcurrency", resolverConcurrency.ToString()); } if (request.TryGetQueryOrForm("resolverMaxStackCount", int.Parse, out int resolverMaxStackCount)) { _dnsWebService._dnsServer.ResolverMaxStackCount = resolverMaxStackCount; clusterParameters.Add("resolverMaxStackCount", resolverMaxStackCount.ToString()); } #endregion #region cache //cache if (request.TryGetQueryOrForm("saveCache", bool.Parse, out bool saveCache)) _dnsWebService._dnsServer.SaveCacheToDisk = saveCache; if (request.TryGetQueryOrForm("serveStale", bool.Parse, out bool serveStale)) _dnsWebService._dnsServer.ServeStale = serveStale; if (request.TryGetQueryOrForm("serveStaleTtl", ZoneFile.ParseTtl, out uint serveStaleTtl)) _dnsWebService._dnsServer.CacheZoneManager.ServeStaleTtl = serveStaleTtl; if (request.TryGetQueryOrForm("serveStaleAnswerTtl", ZoneFile.ParseTtl, out uint serveStaleAnswerTtl)) _dnsWebService._dnsServer.CacheZoneManager.ServeStaleAnswerTtl = serveStaleAnswerTtl; if (request.TryGetQueryOrForm("serveStaleResetTtl", ZoneFile.ParseTtl, out uint serveStaleResetTtl)) _dnsWebService._dnsServer.CacheZoneManager.ServeStaleResetTtl = serveStaleResetTtl; if (request.TryGetQueryOrForm("serveStaleMaxWaitTime", int.Parse, out int serveStaleMaxWaitTime)) _dnsWebService._dnsServer.ServeStaleMaxWaitTime = serveStaleMaxWaitTime; if (request.TryGetQueryOrForm("cacheMaximumEntries", long.Parse, out long cacheMaximumEntries)) _dnsWebService._dnsServer.CacheZoneManager.MaximumEntries = cacheMaximumEntries; if (request.TryGetQueryOrForm("cacheMinimumRecordTtl", ZoneFile.ParseTtl, out uint cacheMinimumRecordTtl)) _dnsWebService._dnsServer.CacheZoneManager.MinimumRecordTtl = cacheMinimumRecordTtl; if (request.TryGetQueryOrForm("cacheMaximumRecordTtl", ZoneFile.ParseTtl, out uint cacheMaximumRecordTtl)) _dnsWebService._dnsServer.CacheZoneManager.MaximumRecordTtl = cacheMaximumRecordTtl; if (request.TryGetQueryOrForm("cacheNegativeRecordTtl", ZoneFile.ParseTtl, out uint cacheNegativeRecordTtl)) _dnsWebService._dnsServer.CacheZoneManager.NegativeRecordTtl = cacheNegativeRecordTtl; if (request.TryGetQueryOrForm("cacheFailureRecordTtl", ZoneFile.ParseTtl, out uint cacheFailureRecordTtl)) _dnsWebService._dnsServer.CacheZoneManager.FailureRecordTtl = cacheFailureRecordTtl; if (request.TryGetQueryOrForm("cachePrefetchEligibility", int.Parse, out int cachePrefetchEligibility)) _dnsWebService._dnsServer.CachePrefetchEligibility = cachePrefetchEligibility; if (request.TryGetQueryOrForm("cachePrefetchTrigger", int.Parse, out int cachePrefetchTrigger)) _dnsWebService._dnsServer.CachePrefetchTrigger = cachePrefetchTrigger; if (request.TryGetQueryOrForm("cachePrefetchSampleIntervalInMinutes", int.Parse, out int cachePrefetchSampleIntervalMinutes)) _dnsWebService._dnsServer.CachePrefetchSampleIntervalMinutes = cachePrefetchSampleIntervalMinutes; if (request.TryGetQueryOrForm("cachePrefetchSampleEligibilityHitsPerHour", int.Parse, out int cachePrefetchSampleEligibilityHitsPerHour)) _dnsWebService._dnsServer.CachePrefetchSampleEligibilityHitsPerHour = cachePrefetchSampleEligibilityHitsPerHour; #endregion #region blocking if (request.TryGetQueryOrForm("enableBlocking", bool.Parse, out bool enableBlocking)) { _dnsWebService._dnsServer.EnableBlocking = enableBlocking; clusterParameters.Add("enableBlocking", enableBlocking.ToString()); } if (request.TryGetQueryOrForm("allowTxtBlockingReport", bool.Parse, out bool allowTxtBlockingReport)) { _dnsWebService._dnsServer.AllowTxtBlockingReport = allowTxtBlockingReport; clusterParameters.Add("allowTxtBlockingReport", allowTxtBlockingReport.ToString()); } if (request.TryGetQueryOrFormArray("blockingBypassList", NetworkAddress.Parse, out NetworkAddress[] blockingBypassList)) { _dnsWebService._dnsServer.BlockingBypassList = blockingBypassList; clusterParameters.Add("blockingBypassList", blockingBypassList.Join()); } if (request.TryGetQueryOrFormEnum("blockingType", out DnsServerBlockingType blockingType)) { _dnsWebService._dnsServer.BlockingType = blockingType; clusterParameters.Add("blockingType", blockingType.ToString()); } if (request.TryGetQueryOrForm("blockingAnswerTtl", ZoneFile.ParseTtl, out uint blockingAnswerTtl)) { _dnsWebService._dnsServer.BlockingAnswerTtl = blockingAnswerTtl; clusterParameters.Add("blockingAnswerTtl", blockingAnswerTtl.ToString()); } if (request.TryGetQueryOrFormArray("customBlockingAddresses", out string[] customBlockingAddresses)) { if (customBlockingAddresses.Length == 0) { _dnsWebService._dnsServer.CustomBlockingARecords = null; _dnsWebService._dnsServer.CustomBlockingAAAARecords = null; } else { List dnsARecords = new List(); List dnsAAAARecords = new List(); foreach (string strAddress in customBlockingAddresses) { if (IPAddress.TryParse(strAddress, out IPAddress customAddress)) { switch (customAddress.AddressFamily) { case AddressFamily.InterNetwork: dnsARecords.Add(new DnsARecordData(customAddress)); break; case AddressFamily.InterNetworkV6: dnsAAAARecords.Add(new DnsAAAARecordData(customAddress)); break; } } } _dnsWebService._dnsServer.CustomBlockingARecords = dnsARecords; _dnsWebService._dnsServer.CustomBlockingAAAARecords = dnsAAAARecords; } clusterParameters.Add("customBlockingAddresses", customBlockingAddresses.Join()); } if (request.TryGetQueryOrFormArray("blockListUrls", out string[] blockListUrls)) { _dnsWebService._dnsServer.BlockListZoneManager.BlockListUrls = blockListUrls; _dnsWebService._dnsServer.BlockListZoneManager.SaveConfigFile(); clusterParameters.Add("blockListUrls", blockListUrls.Join()); } if (request.TryGetQueryOrForm("blockListUpdateIntervalHours", int.Parse, out int blockListUpdateIntervalHours)) { _dnsWebService._dnsServer.BlockListZoneManager.BlockListUpdateIntervalHours = blockListUpdateIntervalHours; _dnsWebService._dnsServer.BlockListZoneManager.SaveConfigFile(); clusterParameters.Add("blockListUpdateIntervalHours", blockListUpdateIntervalHours.ToString()); } #endregion #region proxy & forwarders //proxy & forwarders if (request.TryGetQueryOrFormEnum("proxyType", out NetProxyType proxyType)) { if (proxyType == NetProxyType.None) { _dnsWebService._dnsServer.Proxy = null; } else { NetworkCredential credential = null; if (request.TryGetQueryOrForm("proxyUsername", out string proxyUsername)) { if (proxyUsername.Length > 255) throw new ArgumentException("Proxy username length cannot exceed 255 characters.", nameof(proxyUsername)); string proxyPassword = request.QueryOrForm("proxyPassword"); if (proxyPassword?.Length > 255) throw new ArgumentException("Proxy password length cannot exceed 255 characters.", nameof(proxyPassword)); credential = new NetworkCredential(proxyUsername, proxyPassword); clusterParameters.Add("proxyUsername", proxyUsername); clusterParameters.Add("proxyPassword", proxyPassword ?? ""); } string proxyAddress = request.QueryOrForm("proxyAddress"); string proxyPort = request.QueryOrForm("proxyPort"); _dnsWebService._dnsServer.Proxy = NetProxy.CreateProxy(proxyType, proxyAddress, int.Parse(proxyPort), credential); clusterParameters.Add("proxyAddress", proxyAddress); clusterParameters.Add("proxyPort", proxyPort); if (request.TryGetQueryOrFormArray("proxyBypass", delegate (string value) { return new NetProxyBypassItem(value); }, out NetProxyBypassItem[] proxyBypass)) { _dnsWebService._dnsServer.Proxy.BypassList = proxyBypass; clusterParameters.Add("proxyBypass", proxyBypass.Join()); } } clusterParameters.Add("proxyType", proxyType.ToString()); } if (request.TryGetQueryOrFormArray("forwarders", NameServerAddress.Parse, out NameServerAddress[] forwarders)) { if (forwarders.Length == 0) { _dnsWebService._dnsServer.Forwarders = null; } else { DnsTransportProtocol forwarderProtocol = request.GetQueryOrFormEnum("forwarderProtocol", DnsTransportProtocol.Udp); switch (forwarderProtocol) { case DnsTransportProtocol.Udp: if (proxyType == NetProxyType.Http) throw new DnsWebServiceException("HTTP proxy server can transport only DNS-over-TCP, DNS-over-TLS, or DNS-over-HTTPS forwarder protocols. Use SOCKS5 proxy server for DNS-over-UDP or DNS-over-QUIC forwarder protocols."); break; case DnsTransportProtocol.HttpsJson: forwarderProtocol = DnsTransportProtocol.Https; break; case DnsTransportProtocol.Quic: DnsWebService.ValidateQuicSupport(); if (proxyType == NetProxyType.Http) throw new DnsWebServiceException("HTTP proxy server can transport only DNS-over-TCP, DNS-over-TLS, or DNS-over-HTTPS forwarder protocols. Use SOCKS5 proxy server for DNS-over-UDP or DNS-over-QUIC forwarder protocols."); break; } for (int i = 0; i < forwarders.Length; i++) { if (forwarders[i].Protocol != forwarderProtocol) forwarders[i] = forwarders[i].Clone(forwarderProtocol); } if (!_dnsWebService._dnsServer.Forwarders.ListEquals(forwarders)) _dnsWebService._dnsServer.Forwarders = forwarders; clusterParameters.Add("forwarderProtocol", forwarderProtocol.ToString()); } clusterParameters.Add("forwarders", forwarders.Join()); } if (request.TryGetQueryOrForm("concurrentForwarding", bool.Parse, out bool concurrentForwarding)) { _dnsWebService._dnsServer.ConcurrentForwarding = concurrentForwarding; clusterParameters.Add("concurrentForwarding", concurrentForwarding.ToString()); } if (request.TryGetQueryOrForm("forwarderRetries", int.Parse, out int forwarderRetries)) { _dnsWebService._dnsServer.ForwarderRetries = forwarderRetries; clusterParameters.Add("forwarderRetries", forwarderRetries.ToString()); } if (request.TryGetQueryOrForm("forwarderTimeout", int.Parse, out int forwarderTimeout)) { _dnsWebService._dnsServer.ForwarderTimeout = forwarderTimeout; clusterParameters.Add("forwarderTimeout", forwarderTimeout.ToString()); } if (request.TryGetQueryOrForm("forwarderConcurrency", int.Parse, out int forwarderConcurrency)) { _dnsWebService._dnsServer.ForwarderConcurrency = forwarderConcurrency; clusterParameters.Add("forwarderConcurrency", forwarderConcurrency.ToString()); } #endregion #region logging if (request.TryGetQueryOrFormEnum("loggingType", out LoggingType loggingType)) _dnsWebService._log.LoggingType = loggingType; else if (request.TryGetQueryOrForm("enableLogging", bool.Parse, out bool enableLogging)) _dnsWebService._log.LoggingType = enableLogging ? LoggingType.File : LoggingType.None; if (request.TryGetQueryOrForm("ignoreResolverLogs", bool.Parse, out bool ignoreResolverLogs)) _dnsWebService._dnsServer.ResolverLogManager = ignoreResolverLogs ? null : _dnsWebService._log; if (request.TryGetQueryOrForm("logQueries", bool.Parse, out bool logQueries)) _dnsWebService._dnsServer.QueryLogManager = logQueries ? _dnsWebService._log : null; if (request.TryGetQueryOrForm("useLocalTime", bool.Parse, out bool useLocalTime)) _dnsWebService._log.UseLocalTime = useLocalTime; if (request.TryGetQueryOrForm("logFolder", out string logFolder)) _dnsWebService._log.LogFolder = logFolder; if (request.TryGetQueryOrForm("maxLogFileDays", int.Parse, out int maxLogFileDays)) _dnsWebService._log.MaxLogFileDays = maxLogFileDays; if (request.TryGetQueryOrForm("enableInMemoryStats", bool.Parse, out bool enableInMemoryStats)) _dnsWebService._dnsServer.StatsManager.EnableInMemoryStats = enableInMemoryStats; if (request.TryGetQueryOrForm("maxStatFileDays", int.Parse, out int maxStatFileDays)) _dnsWebService._dnsServer.StatsManager.MaxStatFileDays = maxStatFileDays; #endregion } finally { jsonDocument?.Dispose(); //enforce cluster mandatory TLS requirement if (_dnsWebService._clusterManager.ClusterInitialized) { if (!_dnsWebService._webServiceEnableTls || string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath)) { //force enable TLS with self-signed certificate if cluster is initialized _dnsWebService._webServiceEnableTls = true; _dnsWebService._webServiceUseSelfSignedTlsCertificate = true; } } //TLS actions _dnsWebService.CheckAndLoadSelfSignedCertificate(serverDomainChanged || webServiceLocalAddressesChanged, true); if (_dnsWebService._webServiceEnableTls && string.IsNullOrEmpty(_dnsWebService._webServiceTlsCertificatePath) && !_dnsWebService._webServiceUseSelfSignedTlsCertificate) { //disable TLS _dnsWebService._webServiceEnableTls = false; restartWebService = true; } //cluster update actions if (_dnsWebService._clusterManager.ClusterInitialized) { if (webServiceTlsCertificateChanged || serverDomainChanged || webServiceLocalAddressesChanged) _dnsWebService._clusterManager.UpdateSelfNodeUrlAndCertificate(); } //save config _dnsWebService.SaveConfigFile(); _dnsWebService._dnsServer.SaveConfigFile(); _dnsWebService._dnsServer.BlockListZoneManager.SaveConfigFile(); _dnsWebService._log.SaveConfigFile(); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNS Settings were updated successfully."); //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) { if (_dnsWebService._clusterManager.GetSelfNode().Type == ClusterNodeType.Primary) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodes(); else if (clusterParameters.Count > 0) await _dnsWebService._clusterManager.GetPrimaryNode().SetClusterSettingsAsync(sessionUser, clusterParameters); } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteDnsSettings(jsonWriter); } finally { if (restartDnsService || restartWebService) RestartService(restartDnsService, restartWebService, oldWebServiceLocalAddresses, oldWebServiceHttpPort, oldWebServiceTlsPort); } } public void GetTsigKeyNames(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if ( !_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.View) && !_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify) ) { throw new DnsWebServiceException("Access was denied."); } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("tsigKeyNames"); { jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.TsigKeys is not null) { foreach (KeyValuePair tsigKey in _dnsWebService._dnsServer.TsigKeys) jsonWriter.WriteStringValue(tsigKey.Key); } jsonWriter.WriteEndArray(); } } public async Task BackupSettingsAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; bool authConfig = request.GetQueryOrForm("authConfig", bool.Parse, false); bool clusterConfig = request.GetQueryOrForm("clusterConfig", bool.Parse, false); bool webServiceSettings = request.GetQueryOrForm("webServiceSettings", bool.Parse, false); bool dnsSettings = request.GetQueryOrForm("dnsSettings", bool.Parse, false); bool logSettings = request.GetQueryOrForm("logSettings", bool.Parse, false); bool zones = request.GetQueryOrForm("zones", bool.Parse, false); bool allowedZones = request.GetQueryOrForm("allowedZones", bool.Parse, false); bool blockedZones = request.GetQueryOrForm("blockedZones", bool.Parse, false); bool blockLists = request.GetQueryOrForm("blockLists", bool.Parse, false); bool apps = request.GetQueryOrForm("apps", bool.Parse, false); bool scopes = request.GetQueryOrForm("scopes", bool.Parse, false); bool stats = request.GetQueryOrForm("stats", bool.Parse, false); bool logs = request.GetQueryOrForm("logs", bool.Parse, false); string tmpFile = Path.GetTempFileName(); try { await using (FileStream backupZipStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { //create backup zip await _dnsWebService.BackupConfigAsync(backupZipStream, authConfig, clusterConfig, webServiceSettings, dnsSettings, logSettings, zones, allowedZones, blockedZones, blockLists, apps, scopes, stats, logs); //send zip file backupZipStream.Position = 0; HttpResponse response = context.Response; response.ContentType = "application/zip"; response.ContentLength = backupZipStream.Length; response.Headers.ContentDisposition = "attachment;filename=" + _dnsWebService._dnsServer.ServerDomain + DateTime.UtcNow.ToString("_yyyy-MM-dd_HH-mm-ss") + "_backup.zip"; await using (Stream output = response.Body) { await backupZipStream.CopyToAsync(output); } } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Settings backup zip file was exported."); } public async Task RestoreSettingsAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; bool authConfig = request.GetQueryOrForm("authConfig", bool.Parse, false); bool clusterConfig = request.GetQueryOrForm("clusterConfig", bool.Parse, false); bool webServiceSettings = request.GetQueryOrForm("webServiceSettings", bool.Parse, false); bool dnsSettings = request.GetQueryOrForm("dnsSettings", bool.Parse, false); bool logSettings = request.GetQueryOrForm("logSettings", bool.Parse, false); bool zones = request.GetQueryOrForm("zones", bool.Parse, false); bool allowedZones = request.GetQueryOrForm("allowedZones", bool.Parse, false); bool blockedZones = request.GetQueryOrForm("blockedZones", bool.Parse, false); bool blockLists = request.GetQueryOrForm("blockLists", bool.Parse, false); bool apps = request.GetQueryOrForm("apps", bool.Parse, false); bool scopes = request.GetQueryOrForm("scopes", bool.Parse, false); bool stats = request.GetQueryOrForm("stats", bool.Parse, false); bool logs = request.GetQueryOrForm("logs", bool.Parse, false); bool deleteExistingFiles = request.GetQueryOrForm("deleteExistingFiles", bool.Parse, false); if (!request.HasFormContentType || (request.Form.Files.Count == 0)) throw new DnsWebServiceException("DNS backup zip file is missing."); IReadOnlyList oldWebServiceLocalAddresses = _dnsWebService._webServiceLocalAddresses; int oldWebServiceHttpPort = _dnsWebService._webServiceHttpPort; int oldWebServiceTlsPort = _dnsWebService._webServiceTlsPort; try { //write to temp file string tmpFile = Path.GetTempFileName(); try { await using (FileStream fS = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite)) { await request.Form.Files[0].CopyToAsync(fS); fS.Position = 0; await _dnsWebService.RestoreConfigAsync(fS, authConfig, clusterConfig, webServiceSettings, dnsSettings, logSettings, zones, allowedZones, blockedZones, blockLists, apps, scopes, stats, logs, deleteExistingFiles, context.GetCurrentSession()); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Settings backup zip file was restored."); } } finally { try { File.Delete(tmpFile); } catch (Exception ex) { _dnsWebService._log.Write(ex); } } //trigger cluster update if (_dnsWebService._clusterManager.ClusterInitialized) _dnsWebService._clusterManager.TriggerNotifyAllSecondaryNodesIfPrimarySelfNode(); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); WriteDnsSettings(jsonWriter); } finally { if (dnsSettings || webServiceSettings) RestartService(dnsSettings, webServiceSettings, oldWebServiceLocalAddresses, oldWebServiceHttpPort, oldWebServiceTlsPort); } } public void ForceUpdateBlockLists(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.BlockListZoneManager.ForceUpdateBlockLists(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Block list update was triggered."); if (_dnsWebService._clusterManager.ClusterInitialized) { UserSession session = context.GetCurrentSession(); if ((session.Type == UserSessionType.ApiToken) && session.TokenName.Equals(_dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase)) return; //call from cluster node itself //relay action on all other cluster nodes async ThreadPool.QueueUserWorkItem(async delegate (object state) { try { IReadOnlyDictionary clusterNodes = _dnsWebService._clusterManager.ClusterNodes; List tasks = new List(clusterNodes.Count); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.State == ClusterNodeState.Self) continue; tasks.Add(clusterNode.Value.ForceUpdateBlockListsAsync(sessionUser)); } foreach (Task task in tasks) { try { await task; } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } catch (Exception ex) { _dnsWebService._log.Write(ex); } }); } } public void TemporaryDisableBlocking(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Settings, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); int minutes = context.Request.GetQueryOrForm("minutes", int.Parse); _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlocking(minutes, context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), sessionUser.Username); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("temporaryDisableBlockingTill", _dnsWebService._dnsServer.BlockListZoneManager.TemporaryDisableBlockingTill); if (_dnsWebService._clusterManager.ClusterInitialized) { UserSession session = context.GetCurrentSession(); if ((session.Type == UserSessionType.ApiToken) && session.TokenName.Equals(_dnsWebService._clusterManager.ClusterDomain, StringComparison.OrdinalIgnoreCase)) return; //call from cluster node itself //relay action on all other cluster nodes async ThreadPool.QueueUserWorkItem(async delegate (object state) { try { IReadOnlyDictionary clusterNodes = _dnsWebService._clusterManager.ClusterNodes; List tasks = new List(clusterNodes.Count); foreach (KeyValuePair clusterNode in clusterNodes) { if (clusterNode.Value.State == ClusterNodeState.Self) continue; tasks.Add(clusterNode.Value.TemporaryDisableBlockingAsync(sessionUser, minutes)); } foreach (Task task in tasks) { try { await task; } catch (Exception ex) { _dnsWebService._log.Write(ex); } } } catch (Exception ex) { _dnsWebService._log.Write(ex); } }); } } #endregion } } } ================================================ FILE: DnsServerCore/WebServiceZonesApi.cs ================================================ /* 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.Dns; using DnsServerCore.Dns.Dnssec; using DnsServerCore.Dns.ResourceRecords; using DnsServerCore.Dns.ZoneManagers; using DnsServerCore.Dns.Zones; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; using System.Text.Json; using System.Threading.Tasks; using TechnitiumLibrary; using TechnitiumLibrary.Net; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore { public partial class DnsWebService { class WebServiceZonesApi { #region variables static readonly char[] _commaSeparator = new char[] { ',' }; static readonly char[] _pipeSeparator = new char[] { '|' }; static readonly char[] _commaSpaceSeparator = new char[] { ',', ' ' }; static readonly char[] _newLineSeparator = new char[] { '\r', '\n' }; readonly DnsWebService _dnsWebService; #endregion #region constructor public WebServiceZonesApi(DnsWebService dnsWebService) { _dnsWebService = dnsWebService; } #endregion #region static public static void WriteRecordsAsJson(List records, Utf8JsonWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null) { if (records is null) { jsonWriter.WritePropertyName("records"); jsonWriter.WriteStartArray(); jsonWriter.WriteEndArray(); return; } records.Sort(); Dictionary>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(records); jsonWriter.WritePropertyName("records"); jsonWriter.WriteStartArray(); foreach (KeyValuePair>> groupedByTypeRecords in groupedByDomainRecords) { foreach (KeyValuePair> groupedRecords in groupedByTypeRecords.Value) { foreach (DnsResourceRecord record in groupedRecords.Value) WriteRecordAsJson(record, jsonWriter, authoritativeZoneRecords, zoneInfo); } } jsonWriter.WriteEndArray(); } #endregion #region private private static void WriteRecordAsJson(DnsResourceRecord record, Utf8JsonWriter jsonWriter, bool authoritativeZoneRecords, AuthZoneInfo zoneInfo = null) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", record.Name); if (DnsClient.TryConvertDomainNameToUnicode(record.Name, out string idn)) jsonWriter.WriteString("nameIdn", idn); jsonWriter.WriteString("type", record.Type.ToString()); if (authoritativeZoneRecords) { GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo(); jsonWriter.WriteNumber("ttl", record.TTL); jsonWriter.WriteString("ttlString", ZoneFile.GetTtlString(record.TTL)); jsonWriter.WriteBoolean("disabled", authRecordInfo.Disabled); string comments = authRecordInfo.Comments; if (!string.IsNullOrEmpty(comments)) jsonWriter.WriteString("comments", comments); } else { if (record.IsStale) jsonWriter.WriteString("ttl", "0 (0s)"); else jsonWriter.WriteString("ttl", record.TTL + " (" + ZoneFile.GetTtlString(record.TTL) + ")"); } jsonWriter.WritePropertyName("rData"); jsonWriter.WriteStartObject(); switch (record.Type) { case DnsResourceRecordType.A: { if (record.RDATA is DnsARecordData rdata) { jsonWriter.WriteString("ipAddress", rdata.Address.ToString()); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.NS: { if (record.RDATA is DnsNSRecordData rdata) { jsonWriter.WriteString("nameServer", rdata.NameServer.Length == 0 ? "." : rdata.NameServer); if (DnsClient.TryConvertDomainNameToUnicode(rdata.NameServer, out string nameServerIdn)) jsonWriter.WriteString("nameServerIdn", nameServerIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.CNAME: { if (record.RDATA is DnsCNAMERecordData rdata) { jsonWriter.WriteString("cname", rdata.Domain.Length == 0 ? "." : rdata.Domain); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string cnameIdn)) jsonWriter.WriteString("cnameIdn", cnameIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.SOA: { if (record.RDATA is DnsSOARecordData rdata) { jsonWriter.WriteString("primaryNameServer", rdata.PrimaryNameServer); if (DnsClient.TryConvertDomainNameToUnicode(rdata.PrimaryNameServer, out string primaryNameServerIdn)) jsonWriter.WriteString("primaryNameServerIdn", primaryNameServerIdn); jsonWriter.WriteString("responsiblePerson", rdata.ResponsiblePerson); jsonWriter.WriteNumber("serial", rdata.Serial); if (authoritativeZoneRecords) { jsonWriter.WriteNumber("refresh", rdata.Refresh); jsonWriter.WriteNumber("retry", rdata.Retry); jsonWriter.WriteNumber("expire", rdata.Expire); jsonWriter.WriteNumber("minimum", rdata.Minimum); jsonWriter.WriteString("refreshString", ZoneFile.GetTtlString(rdata.Refresh)); jsonWriter.WriteString("retryString", ZoneFile.GetTtlString(rdata.Retry)); jsonWriter.WriteString("expireString", ZoneFile.GetTtlString(rdata.Expire)); jsonWriter.WriteString("minimumString", ZoneFile.GetTtlString(rdata.Minimum)); } else { jsonWriter.WriteString("refresh", rdata.Refresh + " (" + ZoneFile.GetTtlString(rdata.Refresh) + ")"); jsonWriter.WriteString("retry", rdata.Retry + " (" + ZoneFile.GetTtlString(rdata.Retry) + ")"); jsonWriter.WriteString("expire", rdata.Expire + " (" + ZoneFile.GetTtlString(rdata.Expire) + ")"); jsonWriter.WriteString("minimum", rdata.Minimum + " (" + ZoneFile.GetTtlString(rdata.Minimum) + ")"); } } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } if (authoritativeZoneRecords && (zoneInfo is not null)) { switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: jsonWriter.WriteBoolean("useSerialDateScheme", record.GetAuthSOARecordInfo().UseSoaSerialDateScheme); break; } } } break; case DnsResourceRecordType.PTR: { if (record.RDATA is DnsPTRRecordData rdata) { jsonWriter.WriteString("ptrName", rdata.Domain.Length == 0 ? "." : rdata.Domain); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string ptrNameIdn)) jsonWriter.WriteString("ptrNameIdn", ptrNameIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.MX: { if (record.RDATA is DnsMXRecordData rdata) { jsonWriter.WriteNumber("preference", rdata.Preference); jsonWriter.WriteString("exchange", rdata.Exchange.Length == 0 ? "." : rdata.Exchange); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Exchange, out string exchangeIdn)) jsonWriter.WriteString("exchangeIdn", exchangeIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.TXT: { if (record.RDATA is DnsTXTRecordData rdata) { jsonWriter.WriteString("text", rdata.GetText()); jsonWriter.WriteBoolean("splitText", rdata.CharacterStrings.Count > 1); jsonWriter.WriteStartArray("characterStrings"); foreach (string characterString in rdata.CharacterStrings) jsonWriter.WriteStringValue(characterString); jsonWriter.WriteEndArray(); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.RP: { if (record.RDATA is DnsRPRecordData rdata) { jsonWriter.WriteString("mailbox", rdata.Mailbox); jsonWriter.WriteString("txtDomain", rdata.TxtDomain); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Mailbox, out string txtDomainIdn)) jsonWriter.WriteString("txtDomainIdn", txtDomainIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.AAAA: { if (record.RDATA is DnsAAAARecordData rdata) { jsonWriter.WriteString("ipAddress", rdata.Address.ToString()); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.SRV: { if (record.RDATA is DnsSRVRecordData rdata) { jsonWriter.WriteNumber("priority", rdata.Priority); jsonWriter.WriteNumber("weight", rdata.Weight); jsonWriter.WriteNumber("port", rdata.Port); jsonWriter.WriteString("target", rdata.Target.Length == 0 ? "." : rdata.Target); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Target, out string targetIdn)) jsonWriter.WriteString("targetIdn", targetIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.NAPTR: { if (record.RDATA is DnsNAPTRRecordData rdata) { jsonWriter.WriteNumber("order", rdata.Order); jsonWriter.WriteNumber("preference", rdata.Preference); jsonWriter.WriteString("flags", rdata.Flags); jsonWriter.WriteString("services", rdata.Services); jsonWriter.WriteString("regexp", rdata.Regexp); jsonWriter.WriteString("replacement", rdata.Replacement.Length == 0 ? "." : rdata.Replacement); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Replacement, out string replacementIdn)) jsonWriter.WriteString("replacementIdn", replacementIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.DNAME: { if (record.RDATA is DnsDNAMERecordData rdata) { jsonWriter.WriteString("dname", rdata.Domain.Length == 0 ? "." : rdata.Domain); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string dnameIdn)) jsonWriter.WriteString("dnameIdn", dnameIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.APL: { if (record.RDATA is DnsAPLRecordData rdata) { jsonWriter.WriteStartArray("addressPrefixes"); foreach (DnsAPLRecordData.APItem apItem in rdata.APItems) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("addressFamily", apItem.AddressFamily.ToString()); jsonWriter.WriteNumber("prefix", apItem.Prefix); jsonWriter.WriteBoolean("negation", apItem.Negation); jsonWriter.WriteString("afdPart", apItem.NetworkAddress.Address.ToString()); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.DS: { if (record.RDATA is DnsDSRecordData rdata) { jsonWriter.WriteNumber("keyTag", rdata.KeyTag); jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); jsonWriter.WriteNumber("algorithmNumber", (byte)rdata.Algorithm); jsonWriter.WriteString("digestType", rdata.DigestType.ToString()); jsonWriter.WriteNumber("digestTypeNumber", (byte)rdata.DigestType); jsonWriter.WriteString("digest", Convert.ToHexString(rdata.Digest)); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.SSHFP: { if (record.RDATA is DnsSSHFPRecordData rdata) { jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); jsonWriter.WriteString("fingerprintType", rdata.FingerprintType.ToString()); jsonWriter.WriteString("fingerprint", Convert.ToHexString(rdata.Fingerprint)); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.RRSIG: { if (record.RDATA is DnsRRSIGRecordData rdata) { jsonWriter.WriteString("typeCovered", rdata.TypeCovered.ToString()); jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); jsonWriter.WriteNumber("algorithmNumber", (byte)rdata.Algorithm); jsonWriter.WriteNumber("labels", rdata.Labels); jsonWriter.WriteNumber("originalTtl", rdata.OriginalTtl); jsonWriter.WriteString("signatureExpiration", DateTime.UnixEpoch.AddSeconds(rdata.SignatureExpiration)); jsonWriter.WriteString("signatureInception", DateTime.UnixEpoch.AddSeconds(rdata.SignatureInception)); jsonWriter.WriteNumber("keyTag", rdata.KeyTag); jsonWriter.WriteString("signersName", rdata.SignersName.Length == 0 ? "." : rdata.SignersName); jsonWriter.WriteString("signature", Convert.ToBase64String(rdata.Signature)); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.NSEC: { if (record.RDATA is DnsNSECRecordData rdata) { jsonWriter.WriteString("nextDomainName", rdata.NextDomainName); jsonWriter.WritePropertyName("types"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecordType type in rdata.Types) jsonWriter.WriteStringValue(type.ToString()); jsonWriter.WriteEndArray(); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.DNSKEY: { if (record.RDATA is DnsDNSKEYRecordData rdata) { jsonWriter.WriteString("flags", rdata.Flags.ToString()); jsonWriter.WriteNumber("protocol", rdata.Protocol); jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); jsonWriter.WriteNumber("algorithmNumber", (byte)rdata.Algorithm); jsonWriter.WriteString("publicKey", rdata.PublicKey.ToString()); jsonWriter.WriteNumber("computedKeyTag", rdata.ComputedKeyTag); if (authoritativeZoneRecords) { if ((zoneInfo is not null) && (zoneInfo.Type == AuthZoneType.Primary)) { IReadOnlyCollection dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys; if (dnssecPrivateKeys is not null) { foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) { if (dnssecPrivateKey.KeyTag == rdata.ComputedKeyTag) { jsonWriter.WriteString("dnsKeyState", dnssecPrivateKey.State.ToString()); if (dnssecPrivateKey.State == DnssecPrivateKeyState.Published) { switch (dnssecPrivateKey.KeyType) { case DnssecPrivateKeyType.KeySigningKey: jsonWriter.WriteString("dnsKeyStateReadyBy", dnssecPrivateKey.StateTransitionByWithDelays); break; case DnssecPrivateKeyType.ZoneSigningKey: jsonWriter.WriteString("dnsKeyStateActiveBy", dnssecPrivateKey.StateTransitionByWithDelays); break; } } break; } } } } if (rdata.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint)) { jsonWriter.WritePropertyName("computedDigests"); jsonWriter.WriteStartArray(); { jsonWriter.WriteStartObject(); jsonWriter.WriteString("digestType", "SHA256"); jsonWriter.WriteString("digest", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA256).Digest)); jsonWriter.WriteEndObject(); } { jsonWriter.WriteStartObject(); jsonWriter.WriteString("digestType", "SHA384"); jsonWriter.WriteString("digest", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA384).Digest)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); } } } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.NSEC3: { if (record.RDATA is DnsNSEC3RecordData rdata) { jsonWriter.WriteString("hashAlgorithm", rdata.HashAlgorithm.ToString()); jsonWriter.WriteString("flags", rdata.Flags.ToString()); jsonWriter.WriteNumber("iterations", rdata.Iterations); jsonWriter.WriteString("salt", Convert.ToHexString(rdata.Salt)); jsonWriter.WriteString("nextHashedOwnerName", rdata.NextHashedOwnerName); jsonWriter.WritePropertyName("types"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecordType type in rdata.Types) jsonWriter.WriteStringValue(type.ToString()); jsonWriter.WriteEndArray(); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.NSEC3PARAM: { if (record.RDATA is DnsNSEC3PARAMRecordData rdata) { jsonWriter.WriteString("hashAlgorithm", rdata.HashAlgorithm.ToString()); jsonWriter.WriteString("flags", rdata.Flags.ToString()); jsonWriter.WriteNumber("iterations", rdata.Iterations); jsonWriter.WriteString("salt", Convert.ToHexString(rdata.Salt)); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.TLSA: { if (record.RDATA is DnsTLSARecordData rdata) { jsonWriter.WriteString("certificateUsage", rdata.CertificateUsage.ToString().Replace('_', '-')); jsonWriter.WriteString("selector", rdata.Selector.ToString()); jsonWriter.WriteString("matchingType", rdata.MatchingType.ToString().Replace('_', '-')); jsonWriter.WriteString("certificateAssociationData", Convert.ToHexString(rdata.CertificateAssociationData)); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.ZONEMD: { if (record.RDATA is DnsZONEMDRecordData rdata) { jsonWriter.WriteNumber("serial", rdata.Serial); jsonWriter.WriteString("scheme", rdata.Scheme.ToString()); jsonWriter.WriteString("hashAlgorithm", rdata.HashAlgorithm.ToString()); jsonWriter.WriteString("digest", Convert.ToHexString(rdata.Digest)); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: { if (record.RDATA is DnsSVCBRecordData rdata) { jsonWriter.WriteNumber("svcPriority", rdata.SvcPriority); jsonWriter.WriteString("svcTargetName", rdata.TargetName); jsonWriter.WritePropertyName("svcParams"); jsonWriter.WriteStartObject(); foreach (KeyValuePair svcParam in rdata.SvcParams) jsonWriter.WriteString(svcParam.Key.ToString().ToLowerInvariant().Replace('_', '-'), svcParam.Value.ToString()); jsonWriter.WriteEndObject(); if (authoritativeZoneRecords) { SVCBRecordInfo rrInfo = record.GetAuthSVCBRecordInfo(); jsonWriter.WriteBoolean("autoIpv4Hint", rrInfo.AutoIpv4Hint); jsonWriter.WriteBoolean("autoIpv6Hint", rrInfo.AutoIpv6Hint); } } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.URI: { if (record.RDATA is DnsURIRecordData rdata) { jsonWriter.WriteNumber("priority", rdata.Priority); jsonWriter.WriteNumber("weight", rdata.Weight); jsonWriter.WriteString("uri", rdata.Uri.AbsoluteUri); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.CAA: { if (record.RDATA is DnsCAARecordData rdata) { jsonWriter.WriteNumber("flags", rdata.Flags); jsonWriter.WriteString("tag", rdata.Tag); jsonWriter.WriteString("value", rdata.Value); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.ANAME: { if (record.RDATA is DnsANAMERecordData rdata) { jsonWriter.WriteString("aname", rdata.Domain.Length == 0 ? "." : rdata.Domain); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string anameIdn)) jsonWriter.WriteString("anameIdn", anameIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; case DnsResourceRecordType.FWD: { if (record.RDATA is DnsForwarderRecordData rdata) { jsonWriter.WriteString("protocol", rdata.Protocol.ToString()); jsonWriter.WriteString("forwarder", rdata.Forwarder); jsonWriter.WriteNumber("priority", rdata.Priority); jsonWriter.WriteBoolean("dnssecValidation", rdata.DnssecValidation); jsonWriter.WriteString("proxyType", rdata.ProxyType.ToString()); switch (rdata.ProxyType) { case DnsForwarderRecordProxyType.Http: case DnsForwarderRecordProxyType.Socks5: jsonWriter.WriteString("proxyAddress", rdata.ProxyAddress); jsonWriter.WriteNumber("proxyPort", rdata.ProxyPort); jsonWriter.WriteString("proxyUsername", rdata.ProxyUsername); jsonWriter.WriteString("proxyPassword", rdata.ProxyPassword); break; } } } break; case DnsResourceRecordType.APP: { if (record.RDATA is DnsApplicationRecordData rdata) { jsonWriter.WriteString("appName", rdata.AppName); jsonWriter.WriteString("classPath", rdata.ClassPath); jsonWriter.WriteString("data", rdata.Data); } } break; case DnsResourceRecordType.ALIAS: { if (record.RDATA is DnsALIASRecordData rdata) { jsonWriter.WriteString("type", rdata.Type.ToString()); jsonWriter.WriteString("alias", rdata.Domain.Length == 0 ? "." : rdata.Domain); if (DnsClient.TryConvertDomainNameToUnicode(rdata.Domain, out string aliasIdn)) jsonWriter.WriteString("aliasIdn", aliasIdn); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; default: { if (record.RDATA is DnsUnknownRecordData rdata) { jsonWriter.WriteString("value", BitConverter.ToString(rdata.DATA).Replace('-', ':')); } else { jsonWriter.WriteString("dataType", record.RDATA.GetType().Name); jsonWriter.WriteString("data", record.RDATA.ToString()); } } break; } jsonWriter.WriteEndObject(); jsonWriter.WriteString("dnssecStatus", record.DnssecStatus.ToString()); if (authoritativeZoneRecords) { GenericRecordInfo authRecordInfo = record.GetAuthGenericRecordInfo(); if (authRecordInfo is NSRecordInfo nsRecordInfo) { IReadOnlyList glueRecords = nsRecordInfo.GlueRecords; if (glueRecords is not null) { jsonWriter.WritePropertyName("glueRecords"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecord glueRecord in glueRecords) jsonWriter.WriteStringValue(glueRecord.RDATA.ToString()); jsonWriter.WriteEndArray(); } } jsonWriter.WriteString("lastUsedOn", authRecordInfo.LastUsedOn); jsonWriter.WriteString("lastModified", authRecordInfo.LastModified); jsonWriter.WriteNumber("expiryTtl", authRecordInfo.ExpiryTtl); jsonWriter.WriteString("expiryTtlString", ZoneFile.GetTtlString(authRecordInfo.ExpiryTtl)); } else { CacheRecordInfo cacheRecordInfo = record.GetCacheRecordInfo(); IReadOnlyList glueRecords = cacheRecordInfo.GlueRecords; if (glueRecords is not null) { jsonWriter.WritePropertyName("glueRecords"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecord glueRecord in glueRecords) jsonWriter.WriteStringValue(glueRecord.RDATA.ToString()); jsonWriter.WriteEndArray(); } IReadOnlyList rrsigRecords = cacheRecordInfo.RRSIGRecords; IReadOnlyList nsecRecords = cacheRecordInfo.NSECRecords; if ((rrsigRecords is not null) || (nsecRecords is not null)) { jsonWriter.WritePropertyName("dnssecRecords"); jsonWriter.WriteStartArray(); if (rrsigRecords is not null) { foreach (DnsResourceRecord rrsigRecord in rrsigRecords) jsonWriter.WriteStringValue(rrsigRecord.ToString()); } if (nsecRecords is not null) { foreach (DnsResourceRecord nsecRecord in nsecRecords) jsonWriter.WriteStringValue(nsecRecord.ToString()); } jsonWriter.WriteEndArray(); } NetworkAddress eDnsClientSubnet = cacheRecordInfo.EDnsClientSubnet; if (eDnsClientSubnet is not null) jsonWriter.WriteString("eDnsClientSubnet", eDnsClientSubnet.ToString()); if (record.RDATA is DnsNSRecordData nsRData) { NameServerMetadata metadata = nsRData.Metadata; jsonWriter.WriteStartObject("nameServerMetadata"); jsonWriter.WriteNumber("totalQueries", metadata.TotalQueries); jsonWriter.WriteString("answerRate", Math.Round(metadata.GetAnswerRate(), 2) + "%"); jsonWriter.WriteString("smoothedRoundTripTime", Math.Round(metadata.SRTT, 2) + " ms"); jsonWriter.WriteString("smoothedPenaltyRoundTripTime", Math.Round(metadata.SPRTT, 2) + " ms"); jsonWriter.WriteString("netRoundTripTime", Math.Round(metadata.GetNetRTT(), 2) + " ms"); jsonWriter.WriteEndObject(); } DnsDatagramMetadata responseMetadata = cacheRecordInfo.ResponseMetadata; if (responseMetadata is not null) { jsonWriter.WritePropertyName("responseMetadata"); jsonWriter.WriteStartObject(); jsonWriter.WriteString("nameServer", responseMetadata.NameServer?.ToString()); jsonWriter.WriteString("protocol", (responseMetadata.NameServer is null ? DnsTransportProtocol.Udp : responseMetadata.NameServer.Protocol).ToString()); jsonWriter.WriteString("datagramSize", responseMetadata.DatagramSize + " bytes"); jsonWriter.WriteString("roundTripTime", Math.Round(responseMetadata.RoundTripTime, 2) + " ms"); jsonWriter.WriteEndObject(); } jsonWriter.WriteString("lastUsedOn", cacheRecordInfo.LastUsedOn); } jsonWriter.WriteEndObject(); } private static void WriteZoneInfoAsJson(AuthZoneInfo zoneInfo, Utf8JsonWriter jsonWriter) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("name", zoneInfo.Name); if (DnsClient.TryConvertDomainNameToUnicode(zoneInfo.Name, out string nameIdn)) jsonWriter.WriteString("nameIdn", nameIdn); jsonWriter.WriteString("type", zoneInfo.Type.ToString()); jsonWriter.WriteString("lastModified", zoneInfo.LastModified); jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); jsonWriter.WriteNumber("soaSerial", zoneInfo.ApexZone.GetZoneSoaSerial()); switch (zoneInfo.Type) { case AuthZoneType.Primary: jsonWriter.WriteBoolean("internal", zoneInfo.Internal); break; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: case AuthZoneType.SecondaryForwarder: jsonWriter.WriteString("catalog", zoneInfo.CatalogZoneName); break; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: jsonWriter.WriteString("dnssecStatus", zoneInfo.ApexZone.DnssecStatus.ToString()); jsonWriter.WriteBoolean("hasDnssecPrivateKeys", (zoneInfo.DnssecPrivateKeys is not null) && (zoneInfo.DnssecPrivateKeys.Count > 0)); break; } switch (zoneInfo.Type) { case AuthZoneType.Secondary: jsonWriter.WriteBoolean("validationFailed", zoneInfo.ValidationFailed); break; } switch (zoneInfo.Type) { case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: jsonWriter.WriteString("expiry", zoneInfo.Expiry); jsonWriter.WriteBoolean("isExpired", zoneInfo.IsExpired); jsonWriter.WriteBoolean("syncFailed", zoneInfo.SyncFailed); break; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: if (!zoneInfo.Internal) { string[] notifyFailed = zoneInfo.NotifyFailed; jsonWriter.WriteBoolean("notifyFailed", notifyFailed.Length > 0); jsonWriter.WritePropertyName("notifyFailedFor"); jsonWriter.WriteStartArray(); foreach (string server in notifyFailed) jsonWriter.WriteStringValue(server); jsonWriter.WriteEndArray(); } break; } jsonWriter.WriteEndObject(); } private static void WriteDnssecPrivateKeyAsJson(DnssecPrivateKey dnssecPrivateKey, Utf8JsonWriter jsonWriter) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("keyTag", dnssecPrivateKey.KeyTag); jsonWriter.WriteString("keyType", dnssecPrivateKey.KeyType.ToString()); switch (dnssecPrivateKey.Algorithm) { case DnssecAlgorithm.RSAMD5: case DnssecAlgorithm.RSASHA1: case DnssecAlgorithm.RSASHA1_NSEC3_SHA1: case DnssecAlgorithm.RSASHA256: case DnssecAlgorithm.RSASHA512: jsonWriter.WriteString("algorithm", dnssecPrivateKey.Algorithm.ToString() + " (" + (dnssecPrivateKey as DnssecRsaPrivateKey).KeySize + " bits)"); break; default: jsonWriter.WriteString("algorithm", dnssecPrivateKey.Algorithm.ToString()); break; } jsonWriter.WriteNumber("algorithmNumber", (byte)dnssecPrivateKey.Algorithm); jsonWriter.WriteString("state", dnssecPrivateKey.State.ToString()); jsonWriter.WriteString("stateChangedOn", dnssecPrivateKey.StateChangedOn); if (dnssecPrivateKey.State == DnssecPrivateKeyState.Published) { switch (dnssecPrivateKey.KeyType) { case DnssecPrivateKeyType.KeySigningKey: jsonWriter.WriteString("stateReadyBy", dnssecPrivateKey.StateTransitionByWithDelays); break; case DnssecPrivateKeyType.ZoneSigningKey: jsonWriter.WriteString("stateActiveBy", dnssecPrivateKey.StateTransitionByWithDelays); break; } } jsonWriter.WriteBoolean("isRetiring", dnssecPrivateKey.IsRetiring); jsonWriter.WriteNumber("rolloverDays", dnssecPrivateKey.RolloverDays); jsonWriter.WriteEndObject(); } private static string[] DecodeCharacterStrings(string text) { string[] characterStrings = text.Split(_newLineSeparator, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < characterStrings.Length; i++) characterStrings[i] = Unescape(characterStrings[i]); return characterStrings; } private static string Unescape(string text) { StringBuilder sb = new StringBuilder(text.Length); for (int i = 0, j; i < text.Length; i++) { char c = text[i]; if (c == '\\') { j = i + 1; if (j == text.Length) { sb.Append(c); break; } char next = text[j]; switch (next) { case 'n': sb.Append('\n'); break; case 'r': sb.Append('\r'); break; case 't': sb.Append('\t'); break; case '\\': sb.Append('\\'); break; default: sb.Append(c).Append(next); break; } i++; } else { sb.Append(c); } } return sb.ToString(); } private static string GetSvcbTargetName(DnsResourceRecord svcbRecord) { DnsSVCBRecordData rData = svcbRecord.RDATA as DnsSVCBRecordData; if (rData.TargetName.Length > 0) return rData.TargetName; if (rData.SvcPriority == 0) //alias mode return null; //service mode return svcbRecord.Name; } private void ResolveSvcbAutoHints(string zoneName, DnsResourceRecord svcbRecord, bool resolveIpv4Hint, bool resolveIpv6Hint, Dictionary svcParams, IReadOnlyCollection importRecords = null) { string targetName = GetSvcbTargetName(svcbRecord); if (targetName is not null) ResolveSvcbAutoHints(zoneName, targetName, resolveIpv4Hint, resolveIpv6Hint, svcParams, importRecords); } private void ResolveSvcbAutoHints(string zoneName, string targetName, bool resolveIpv4Hint, bool resolveIpv6Hint, Dictionary svcParams, IReadOnlyCollection importRecords = null) { if (resolveIpv4Hint) { List ipv4Hint = new List(); IReadOnlyList records = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneName, targetName, DnsResourceRecordType.A); foreach (DnsResourceRecord record in records) { if (record.GetAuthGenericRecordInfo().Disabled) continue; ipv4Hint.Add((record.RDATA as DnsARecordData).Address); } if (importRecords is not null) { foreach (DnsResourceRecord record in importRecords) { if (record.Type != DnsResourceRecordType.A) continue; if (record.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase)) { IPAddress address = (record.RDATA as DnsARecordData).Address; if (!ipv4Hint.Contains(address)) ipv4Hint.Add(address); } } } if (ipv4Hint.Count > 0) svcParams[DnsSvcParamKey.IPv4Hint] = new DnsSvcIPv4HintParamValue(ipv4Hint); else svcParams.Remove(DnsSvcParamKey.IPv4Hint); } if (resolveIpv6Hint) { List ipv6Hint = new List(); IReadOnlyList records = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneName, targetName, DnsResourceRecordType.AAAA); foreach (DnsResourceRecord record in records) { if (record.GetAuthGenericRecordInfo().Disabled) continue; ipv6Hint.Add((record.RDATA as DnsAAAARecordData).Address); } if (importRecords is not null) { foreach (DnsResourceRecord record in importRecords) { if (record.Type != DnsResourceRecordType.AAAA) continue; if (record.Name.Equals(targetName, StringComparison.OrdinalIgnoreCase)) { IPAddress address = (record.RDATA as DnsAAAARecordData).Address; if (!ipv6Hint.Contains(address)) ipv6Hint.Add(address); } } } if (ipv6Hint.Count > 0) svcParams[DnsSvcParamKey.IPv6Hint] = new DnsSvcIPv6HintParamValue(ipv6Hint); else svcParams.Remove(DnsSvcParamKey.IPv6Hint); } } private void UpdateSvcbAutoHints(string zoneName, string targetName, bool resolveIpv4Hint, bool resolveIpv6Hint) { List allSvcbRecords = new List(); _dnsWebService._dnsServer.AuthZoneManager.ListAllZoneRecords(zoneName, [DnsResourceRecordType.SVCB, DnsResourceRecordType.HTTPS], allSvcbRecords); foreach (DnsResourceRecord record in allSvcbRecords) { SVCBRecordInfo info = record.GetAuthSVCBRecordInfo(); if ((info.AutoIpv4Hint && resolveIpv4Hint) || (info.AutoIpv6Hint && resolveIpv6Hint)) { string scvbTargetName = GetSvcbTargetName(record); if (targetName.Equals(scvbTargetName, StringComparison.OrdinalIgnoreCase)) { DnsSVCBRecordData oldRData = record.RDATA as DnsSVCBRecordData; Dictionary newSvcParams = new Dictionary(oldRData.SvcParams); ResolveSvcbAutoHints(zoneName, targetName, resolveIpv4Hint, resolveIpv6Hint, newSvcParams); DnsSVCBRecordData newRData = new DnsSVCBRecordData(oldRData.SvcPriority, oldRData.TargetName, newSvcParams); DnsResourceRecord newRecord = new DnsResourceRecord(record.Name, record.Type, record.Class, record.TTL, newRData) { Tag = record.Tag }; _dnsWebService._dnsServer.AuthZoneManager.UpdateRecord(zoneName, record, newRecord); } } } } private async Task> ReadRecordsToImportFromAsync(string zoneName, AuthZoneType zoneType, string catalogZoneName, bool overwrite, TextReader zoneFile) { List records = await ZoneFile.ReadZoneFileFromAsync(zoneFile, zoneName, _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl); List newRecords = new List(records.Count); foreach (DnsResourceRecord record in records) { if (record.Class != DnsClass.IN) throw new DnsWebServiceException("Cannot import records: only IN class is supported by the DNS server."); if (!AuthZoneManager.DomainBelongsToZone(zoneName, record.Name)) { switch (record.Type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: continue; //glue records default: throw new DnsServerException("Cannot import records: the domain name '" + record.Name + "' does not belong to the zone '" + zoneName + "'."); } } bool disabled = false; string comments = null; if (record.Tag is string tagValue) { if (tagValue.TrimStart().StartsWith('{')) { try { using JsonDocument jsonDocument = JsonDocument.Parse(tagValue); JsonElement json = jsonDocument.RootElement; if (json.TryGetProperty("disabled", out JsonElement jsonDisabled)) disabled = jsonDisabled.ValueKind == JsonValueKind.True; if (json.TryGetProperty("comments", out JsonElement jsonComments) && (jsonComments.ValueKind == JsonValueKind.String)) comments = jsonComments.GetString(); } catch { comments = tagValue.Replace("\\r", "").Replace("\\n", "\n"); } } else { comments = tagValue.Replace("\\r", "").Replace("\\n", "\n"); } } switch (record.Type) { case DnsResourceRecordType.DNSKEY: case DnsResourceRecordType.RRSIG: case DnsResourceRecordType.NSEC: case DnsResourceRecordType.NSEC3: case DnsResourceRecordType.NSEC3PARAM: continue; //skip DNSSEC records case DnsResourceRecordType.NS: { if (record.Tag is string) { NSRecordInfo rrInfo = new NSRecordInfo(); rrInfo.Disabled = disabled; rrInfo.Comments = comments; record.Tag = rrInfo; } record.SyncGlueRecords(records); newRecords.Add(record); } break; case DnsResourceRecordType.SOA: { if (record.Tag is string) { SOARecordInfo rrInfo = new SOARecordInfo(); rrInfo.Comments = comments; record.Tag = rrInfo; } newRecords.Add(record); } break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: { if (record.Tag is string) { SVCBRecordInfo rrInfo = new SVCBRecordInfo(); rrInfo.Disabled = disabled; rrInfo.Comments = comments; record.Tag = rrInfo; } if (record.RDATA is DnsSVCBRecordData rdata && (rdata.AutoIpv4Hint || rdata.AutoIpv6Hint)) { if (rdata.AutoIpv4Hint) record.GetAuthSVCBRecordInfo().AutoIpv4Hint = true; if (rdata.AutoIpv6Hint) record.GetAuthSVCBRecordInfo().AutoIpv6Hint = true; Dictionary svcParams = new Dictionary(rdata.SvcParams); DnsResourceRecord newRecord = new DnsResourceRecord(record.Name, record.Type, record.Class, record.TTL, new DnsSVCBRecordData(rdata.SvcPriority, rdata.TargetName, svcParams)) { Tag = record.Tag }; ResolveSvcbAutoHints(zoneName, record, rdata.AutoIpv4Hint, rdata.AutoIpv6Hint, svcParams, records); newRecords.Add(newRecord); break; } newRecords.Add(record); } break; default: { if (record.Tag is string) { GenericRecordInfo rrInfo = new GenericRecordInfo(); rrInfo.Disabled = disabled; rrInfo.Comments = comments; record.Tag = rrInfo; } newRecords.Add(record); } break; } } //validate records if ((zoneType == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneName)) { int nsCount = 0; foreach (DnsResourceRecord newRecord in newRecords) { switch (newRecord.Type) { case DnsResourceRecordType.NS: if (zoneName.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) { NSRecordInfo recordInfo = newRecord.GetAuthNSRecordInfo(); if (recordInfo.Disabled) throw new DnsWebServiceException("Cannot import disabled NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); if (recordInfo.GlueRecords is not null) throw new DnsWebServiceException("Cannot import NS records with glue addresses for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); string nsDomain = (newRecord.RDATA as DnsNSRecordData).NameServer; bool found = false; foreach (KeyValuePair clusterNode in _dnsWebService._clusterManager.ClusterNodes) { if (nsDomain.Equals(clusterNode.Value.Name, StringComparison.OrdinalIgnoreCase)) { found = true; break; } } if (!found) throw new DnsWebServiceException("Cannot import NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); } nsCount++; break; case DnsResourceRecordType.SOA: DnsSOARecordData soa = newRecord.RDATA as DnsSOARecordData; if (!soa.PrimaryNameServer.Equals(_dnsWebService._dnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Cannot import SOA record for Primary zones that are members of the Cluster Catalog zone. The SOA primary name server field must match the Cluster Primary node's domain name."); break; } } if (overwrite) { if ((nsCount > 0) && (nsCount != _dnsWebService._clusterManager.ClusterNodes.Count)) //check attempt to replace NS records throw new DnsWebServiceException("Cannot import NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); } else { if (nsCount > 0) //check attempt to add NS records throw new DnsWebServiceException("Cannot import NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); } } return newRecords; } #endregion #region public public void ListZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); IReadOnlyList zoneInfoList = _dnsWebService._dnsServer.AuthZoneManager.GetZones(delegate (AuthZoneInfo zoneInfo) { return _dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View); }); if (request.TryGetQueryOrForm("pageNumber", int.Parse, out int pageNumber)) { int zonesPerPage = request.GetQueryOrForm("zonesPerPage", int.Parse, 10); int totalPages; int totalZones = zoneInfoList.Count; if (totalZones > 0) { if (pageNumber == 0) pageNumber = 1; totalPages = (totalZones / zonesPerPage) + (totalZones % zonesPerPage > 0 ? 1 : 0); if ((pageNumber > totalPages) || (pageNumber < 0)) pageNumber = totalPages; int start = (pageNumber - 1) * zonesPerPage; int end = Math.Min(start + zonesPerPage, totalZones); List zoneInfoPageList = new List(end - start); for (int i = start; i < end; i++) zoneInfoPageList.Add(zoneInfoList[i]); zoneInfoList = zoneInfoPageList; } else { pageNumber = 0; totalPages = 0; } jsonWriter.WriteNumber("pageNumber", pageNumber); jsonWriter.WriteNumber("totalPages", totalPages); jsonWriter.WriteNumber("totalZones", totalZones); } jsonWriter.WritePropertyName("zones"); jsonWriter.WriteStartArray(); foreach (AuthZoneInfo zoneInfo in zoneInfoList) WriteZoneInfoAsJson(zoneInfo, jsonWriter); jsonWriter.WriteEndArray(); } public void ListCatalogZones(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); IReadOnlyList catalogZoneInfoList = _dnsWebService._dnsServer.AuthZoneManager.GetCatalogZones(delegate (AuthZoneInfo catalogZoneInfo) { return !catalogZoneInfo.Disabled && _dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify); }); jsonWriter.WritePropertyName("catalogZoneNames"); jsonWriter.WriteStartArray(); foreach (AuthZoneInfo catalogZoneInfo in catalogZoneInfoList) jsonWriter.WriteStringValue(catalogZoneInfo.Name); jsonWriter.WriteEndArray(); } public async Task CreateZoneAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrFormAlt("zone", "domain"); if (IPAddress.TryParse(zoneName, out IPAddress ipAddress)) { zoneName = ipAddress.GetReverseDomain().ToLowerInvariant(); } else { if (zoneName.Contains('/')) { string[] parts = zoneName.Split('/'); if ((parts.Length == 2) && IPAddress.TryParse(parts[0], out ipAddress) && int.TryParse(parts[1], out int subnetMaskWidth)) zoneName = Zone.GetReverseZone(ipAddress, subnetMaskWidth); } else { zoneName = zoneName.Trim('.'); } if (zoneName.Contains('*')) throw new DnsWebServiceException("Domain name for a zone cannot contain wildcard character."); foreach (char invalidChar in Path.GetInvalidFileNameChars()) { if (zoneName.Contains(invalidChar)) throw new DnsWebServiceException("The zone name contains an invalid character: " + invalidChar); } if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); } AuthZoneType type = request.GetQueryOrFormEnum("type", AuthZoneType.Primary); string catalogZoneName = request.GetQueryOrForm("catalog", null); //read records to import, if any List importRecords = null; switch (type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: if (request.HasFormContentType && (request.Form.Files.Count > 0)) { using (TextReader zoneFile = new StreamReader(request.Form.Files[0].OpenReadStream())) { importRecords = await ReadRecordsToImportFromAsync(zoneName, type, catalogZoneName, false, zoneFile); } } break; } //create zone AuthZoneInfo zoneInfo; switch (type) { case AuthZoneType.Primary: { bool useSoaSerialDateScheme = request.GetQueryOrForm("useSoaSerialDateScheme", bool.Parse, _dnsWebService._dnsServer.AuthZoneManager.UseSoaSerialDateScheme); AuthZoneInfo catalogZoneInfo = null; if (catalogZoneName is not null) { catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName); if (catalogZoneInfo is null) throw new DnsWebServiceException("No such Catalog zone was found: " + catalogZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied to use Catalog zone: " + catalogZoneInfo.Name); } zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(zoneName, useSoaSerialDateScheme); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); //add membership for catalog zone if (catalogZoneInfo is not null) { _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo); if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneInfo.Name)) _dnsWebService._clusterManager.UpdateClusterRecordsFor(zoneInfo); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Authoritative Primary zone was created: " + zoneInfo.DisplayName); } break; case AuthZoneType.Secondary: { string primaryNameServerAddresses = request.GetQueryOrForm("primaryNameServerAddresses", null); DnsTransportProtocol primaryZoneTransferProtocol = request.GetQueryOrFormEnum("zoneTransferProtocol", DnsTransportProtocol.Tcp); string primaryZoneTransferTsigKeyName = request.GetQueryOrForm("tsigKeyName", null); bool validateZone = request.GetQueryOrForm("validateZone", bool.Parse, false); if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); AuthZoneInfo catalogZoneInfo = null; if (catalogZoneName is not null) { catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName); if (catalogZoneInfo is null) throw new DnsWebServiceException("No such Catalog zone was found: " + catalogZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied to use Catalog zone: " + catalogZoneInfo.Name); } zoneInfo = await _dnsWebService._dnsServer.AuthZoneManager.CreateSecondaryZoneAsync(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName, validateZone); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); //add membership for catalog zone if (catalogZoneInfo is not null) { _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo); zoneInfo.OverrideCatalogPrimaryNameServers = true; //always true for secondary member zones } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Authoritative Secondary zone was created: " + zoneInfo.DisplayName); } break; case AuthZoneType.Stub: { string primaryNameServerAddresses = request.GetQueryOrForm("primaryNameServerAddresses", null); AuthZoneInfo catalogZoneInfo = null; if (catalogZoneName is not null) { catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName); if (catalogZoneInfo is null) throw new DnsWebServiceException("No such Catalog zone was found: " + catalogZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied to use Catalog zone: " + catalogZoneInfo.Name); } zoneInfo = await _dnsWebService._dnsServer.AuthZoneManager.CreateStubZoneAsync(zoneName, primaryNameServerAddresses); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); //add membership for catalog zone if (catalogZoneInfo is not null) _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Stub zone was created: " + zoneInfo.DisplayName); } break; case AuthZoneType.Forwarder: { bool initializeForwarder = request.GetQueryOrForm("initializeForwarder", bool.Parse, true); AuthZoneInfo catalogZoneInfo = null; if (catalogZoneName is not null) { catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName); if (catalogZoneInfo is null) throw new DnsWebServiceException("No such Catalog zone was found: " + catalogZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied to use Catalog zone: " + catalogZoneInfo.Name); } if (initializeForwarder) { DnsTransportProtocol forwarderProtocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); string forwarder = request.GetQueryOrForm("forwarder"); bool dnssecValidation = request.GetQueryOrForm("dnssecValidation", bool.Parse, false); DnsForwarderRecordProxyType proxyType = request.GetQueryOrFormEnum("proxyType", DnsForwarderRecordProxyType.DefaultProxy); string proxyAddress = null; ushort proxyPort = 0; string proxyUsername = null; string proxyPassword = null; switch (proxyType) { case DnsForwarderRecordProxyType.Http: case DnsForwarderRecordProxyType.Socks5: proxyAddress = request.GetQueryOrForm("proxyAddress"); proxyPort = request.GetQueryOrForm("proxyPort", ushort.Parse); proxyUsername = request.QueryOrForm("proxyUsername"); proxyPassword = request.QueryOrForm("proxyPassword"); break; } if (forwarderProtocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateForwarderZone(zoneName, forwarderProtocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, null); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); } else { zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateForwarderZone(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); } //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); //add membership for catalog zone if (catalogZoneInfo is not null) _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Forwarder zone was created: " + zoneInfo.DisplayName); } break; case AuthZoneType.SecondaryForwarder: { string primaryNameServerAddresses = request.GetQueryOrForm("primaryNameServerAddresses"); DnsTransportProtocol primaryZoneTransferProtocol = request.GetQueryOrFormEnum("zoneTransferProtocol", DnsTransportProtocol.Tcp); string primaryZoneTransferTsigKeyName = request.GetQueryOrForm("tsigKeyName", null); if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateSecondaryForwarderZone(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Secondary Forwarder zone was created: " + zoneInfo.DisplayName); } break; case AuthZoneType.Catalog: { zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateCatalogZone(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Catalog zone was created: " + zoneInfo.DisplayName); } break; case AuthZoneType.SecondaryCatalog: { string primaryNameServerAddresses = request.GetQueryOrForm("primaryNameServerAddresses"); DnsTransportProtocol primaryZoneTransferProtocol = request.GetQueryOrFormEnum("zoneTransferProtocol", DnsTransportProtocol.Tcp); string primaryZoneTransferTsigKeyName = request.GetQueryOrForm("tsigKeyName", null); if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreateSecondaryCatalogZone(zoneName, primaryNameServerAddresses, primaryZoneTransferProtocol, primaryZoneTransferTsigKeyName); if (zoneInfo is null) throw new DnsWebServiceException("Zone already exists: " + zoneName); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Secondary Catalog zone was created: " + zoneInfo.DisplayName); } break; default: throw new NotSupportedException("Zone type not supported."); } //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zones _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name); //import records, if any if (importRecords is not null) { //delete existing NS/FWD record switch (type) { case AuthZoneType.Primary: _dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, zoneInfo.Name, DnsResourceRecordType.NS); break; case AuthZoneType.Forwarder: _dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, zoneInfo.Name, DnsResourceRecordType.FWD); break; } //import records _dnsWebService._dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, importRecords, false, false); } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("domain", string.IsNullOrEmpty(zoneInfo.Name) ? "." : zoneInfo.Name); } public async Task ImportZoneAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); bool overwrite = request.GetQueryOrForm("overwrite", bool.Parse, true); bool overwriteSoaSerial = request.GetQueryOrForm("overwriteSoaSerial", bool.Parse, false); TextReader textReader; switch (request.ContentType?.ToLowerInvariant()) { case "application/x-www-form-urlencoded": string zoneRecords = request.GetQueryOrForm("records"); textReader = new StringReader(zoneRecords); break; case "text/plain": textReader = new StreamReader(request.Body); break; default: if (!request.HasFormContentType || (request.Form.Files.Count == 0)) throw new DnsWebServiceException("The zone file to import is missing."); textReader = new StreamReader(request.Form.Files[0].OpenReadStream()); break; } List records; using (TextReader zoneFile = textReader) { records = await ReadRecordsToImportFromAsync(zoneInfo.Name, zoneInfo.Type, zoneInfo.CatalogZoneName, overwrite, zoneFile); } _dnsWebService._dnsServer.AuthZoneManager.ImportRecords(zoneInfo.Name, records, overwrite, overwriteSoaSerial); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Total " + records.Count + " record(s) were imported successfully into " + zoneInfo.TypeName + " zone: " + zoneInfo.DisplayName); } public async Task ExportZoneAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); List records = new List(); _dnsWebService._dnsServer.AuthZoneManager.ListAllZoneRecords(zoneInfo.Name, records); foreach (DnsResourceRecord record in records) { switch (record.Type) { case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: SVCBRecordInfo info = record.GetAuthSVCBRecordInfo(); if (info.AutoIpv4Hint) (record.RDATA as DnsSVCBRecordData).AutoIpv4Hint = true; if (info.AutoIpv6Hint) (record.RDATA as DnsSVCBRecordData).AutoIpv6Hint = true; break; } } HttpResponse response = context.Response; response.ContentType = "text/plain"; response.Headers.ContentDisposition = "attachment;filename=" + (zoneInfo.Name.Length == 0 ? "root.zone" : zoneInfo.Name + ".zone"); await using (StreamWriter sW = new StreamWriter(response.Body)) { await ZoneFile.WriteZoneFileToAsync(sW, zoneInfo.Name, records, delegate (DnsResourceRecord record) { if (record.Tag is null) return null; GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo(); if (recordInfo.Disabled || ((recordInfo.Comments is not null) && recordInfo.Comments.TrimStart().StartsWith('{'))) { using (MemoryStream mS = new MemoryStream()) { Utf8JsonWriter jsonWriter = new Utf8JsonWriter(mS); jsonWriter.WriteStartObject(); jsonWriter.WriteBoolean("disabled", recordInfo.Disabled); jsonWriter.WriteString("comments", recordInfo.Comments); jsonWriter.WriteEndObject(); jsonWriter.Flush(); return Encoding.UTF8.GetString(mS.ToArray()); } } return recordInfo.Comments?.Replace("\r", "").Replace("\n", "\\n"); }); } } public void CloneZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); string sourceZoneName = request.GetQueryOrForm("sourceZone").Trim('.'); if (DnsClient.IsDomainNameUnicode(sourceZoneName)) sourceZoneName = DnsClient.ConvertDomainNameToAscii(sourceZoneName); AuthZoneInfo sourceZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(sourceZoneName); if (sourceZoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + sourceZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sourceZoneInfo.Name, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CloneZone(zoneName, sourceZoneInfo.Name); //clone user/group permissions from source zone Permission sourceZonePermissions = _dnsWebService._authManager.GetPermission(PermissionSection.Zones, sourceZoneInfo.Name); foreach (KeyValuePair userPermission in sourceZonePermissions.UserPermissions) _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, userPermission.Key, userPermission.Value); foreach (KeyValuePair groupPermissions in sourceZonePermissions.GroupPermissions) _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, groupPermissions.Key, groupPermissions.Value); //set default permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, zoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + sourceZoneInfo.TypeName + " zone '" + sourceZoneInfo.DisplayName + "' was cloned as '" + zoneInfo.DisplayName + "' sucessfully."); } public void ConvertZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); AuthZoneType type = request.GetQueryOrFormEnum("type"); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterPrimaryZone(zoneInfo.Name)) throw new DnsWebServiceException("Cannot convert the Cluster Primary zone '" + zoneInfo.DisplayName + "'."); _dnsWebService._dnsServer.AuthZoneManager.ConvertZoneTypeTo(zoneInfo.Name, type); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' was converted to " + AuthZoneInfo.GetZoneTypeName(type) + " zone sucessfully."); } public void SignPrimaryZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string algorithm = request.GetQueryOrForm("algorithm"); string pemKskPrivateKey = request.GetQueryOrForm("pemKskPrivateKey", null); string pemZskPrivateKey = request.GetQueryOrForm("pemZskPrivateKey", null); uint dnsKeyTtl = request.GetQueryOrForm("dnsKeyTtl", ZoneFile.ParseTtl, 3600u); ushort zskRolloverDays = request.GetQueryOrForm("zskRolloverDays", ushort.Parse, Convert.ToUInt16(pemZskPrivateKey is null ? 30 : 0)); bool useNSEC3 = false; string strNxProof = request.QueryOrForm("nxProof"); if (!string.IsNullOrEmpty(strNxProof)) { switch (strNxProof.ToUpper()) { case "NSEC": useNSEC3 = false; break; case "NSEC3": useNSEC3 = true; break; default: throw new NotSupportedException("Non-existence proof type is not supported: " + strNxProof); } } ushort iterations = 0; byte saltLength = 0; if (useNSEC3) { iterations = request.GetQueryOrForm("iterations", ushort.Parse, 0); saltLength = request.GetQueryOrForm("saltLength", byte.Parse, 0); } DnssecPrivateKey kskPrivateKey; DnssecPrivateKey zskPrivateKey; switch (algorithm.ToUpper()) { case "RSA": { string hashAlgorithm = request.GetQueryOrForm("hashAlgorithm"); DnssecAlgorithm dnssecAlgorithm; switch (hashAlgorithm.ToUpper()) { case "MD5": dnssecAlgorithm = DnssecAlgorithm.RSAMD5; break; case "SHA1": dnssecAlgorithm = DnssecAlgorithm.RSASHA1; break; case "SHA256": dnssecAlgorithm = DnssecAlgorithm.RSASHA256; break; case "SHA512": dnssecAlgorithm = DnssecAlgorithm.RSASHA512; break; default: throw new NotSupportedException("Hash algorithm is not supported: " + hashAlgorithm); } if (pemKskPrivateKey is null) kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, request.GetQueryOrForm("kskKeySize", int.Parse)); else kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey); if (pemZskPrivateKey is null) zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, request.GetQueryOrForm("zskKeySize", int.Parse)); else zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey); } break; case "ECDSA": { string curve = request.GetQueryOrForm("curve"); DnssecAlgorithm dnssecAlgorithm; switch (curve.ToUpper()) { case "P256": dnssecAlgorithm = DnssecAlgorithm.ECDSAP256SHA256; break; case "P384": dnssecAlgorithm = DnssecAlgorithm.ECDSAP384SHA384; break; default: throw new NotSupportedException("ECDSA curve is not supported: " + curve); } if (pemKskPrivateKey is null) kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey); else kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey); if (pemZskPrivateKey is null) zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey); else zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey); } break; case "EDDSA": { string curve = request.GetQueryOrForm("curve"); DnssecAlgorithm dnssecAlgorithm; switch (curve.ToUpper()) { case "ED25519": dnssecAlgorithm = DnssecAlgorithm.ED25519; break; case "ED448": dnssecAlgorithm = DnssecAlgorithm.ED448; break; default: throw new NotSupportedException("EdDSA curve is not supported: " + curve); } if (pemKskPrivateKey is null) kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey); else kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey); if (pemZskPrivateKey is null) zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey); else zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey); } break; default: throw new NotSupportedException("Algorithm is not supported: " + algorithm); } zskPrivateKey.RolloverDays = zskRolloverDays; _dnsWebService._dnsServer.AuthZoneManager.SignPrimaryZone(zoneName, kskPrivateKey, zskPrivateKey, dnsKeyTtl, useNSEC3, iterations, saltLength); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone was signed successfully: " + zoneName); } public void UnsignPrimaryZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.AuthZoneManager.UnsignPrimaryZone(zoneName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone was unsigned successfully: " + zoneName); } public void GetPrimaryZoneDsInfo(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (zoneInfo.Type != AuthZoneType.Primary) throw new DnsWebServiceException("The zone must be a primary zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); if (zoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.Unsigned) throw new DnsWebServiceException("The zone must be signed with DNSSEC."); IReadOnlyList dnsKeyRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.DNSKEY); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("name", zoneInfo.Name); jsonWriter.WriteString("type", zoneInfo.Type.ToString()); jsonWriter.WriteBoolean("internal", zoneInfo.Internal); jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); jsonWriter.WriteString("dnssecStatus", zoneInfo.ApexZone.DnssecStatus.ToString()); jsonWriter.WritePropertyName("dsRecords"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecord record in dnsKeyRecords) { if (record.RDATA is DnsDNSKEYRecordData rdata && rdata.Flags.HasFlag(DnsDnsKeyFlag.SecureEntryPoint)) { jsonWriter.WriteStartObject(); jsonWriter.WriteNumber("keyTag", rdata.ComputedKeyTag); IReadOnlyCollection dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys; if (dnssecPrivateKeys is not null) { foreach (DnssecPrivateKey dnssecPrivateKey in dnssecPrivateKeys) { if ((dnssecPrivateKey.KeyType == DnssecPrivateKeyType.KeySigningKey) && (dnssecPrivateKey.KeyTag == rdata.ComputedKeyTag)) { jsonWriter.WriteString("dnsKeyState", dnssecPrivateKey.State.ToString()); if (dnssecPrivateKey.State == DnssecPrivateKeyState.Published) jsonWriter.WriteString("dnsKeyStateReadyBy", dnssecPrivateKey.StateTransitionByWithDelays); jsonWriter.WriteBoolean("isRetiring", dnssecPrivateKey.IsRetiring); break; } } } jsonWriter.WriteString("algorithm", rdata.Algorithm.ToString()); jsonWriter.WriteNumber("algorithmNumber", (byte)rdata.Algorithm); jsonWriter.WriteString("publicKey", rdata.PublicKey.ToString()); jsonWriter.WritePropertyName("digests"); jsonWriter.WriteStartArray(); { jsonWriter.WriteStartObject(); jsonWriter.WriteString("digestType", "SHA256"); jsonWriter.WriteString("digestTypeNumber", "2"); jsonWriter.WriteString("digest", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA256).Digest)); jsonWriter.WriteEndObject(); } { jsonWriter.WriteStartObject(); jsonWriter.WriteString("digestType", "SHA384"); jsonWriter.WriteString("digestTypeNumber", "4"); jsonWriter.WriteString("digest", Convert.ToHexString(rdata.CreateDS(record.Name, DnssecDigestType.SHA384).Digest)); jsonWriter.WriteEndObject(); } jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); } } jsonWriter.WriteEndArray(); } public void GetPrimaryZoneDnssecProperties(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (zoneInfo.Type != AuthZoneType.Primary) throw new DnsWebServiceException("The zone must be a primary zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("name", zoneInfo.Name); jsonWriter.WriteString("type", zoneInfo.Type.ToString()); jsonWriter.WriteBoolean("internal", zoneInfo.Internal); jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); jsonWriter.WriteString("dnssecStatus", zoneInfo.ApexZone.DnssecStatus.ToString()); if (zoneInfo.ApexZone.DnssecStatus == AuthZoneDnssecStatus.SignedWithNSEC3) { IReadOnlyList nsec3ParamRecords = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.NSEC3PARAM); DnsNSEC3PARAMRecordData nsec3Param = nsec3ParamRecords[0].RDATA as DnsNSEC3PARAMRecordData; jsonWriter.WriteNumber("nsec3Iterations", nsec3Param.Iterations); jsonWriter.WriteNumber("nsec3SaltLength", nsec3Param.Salt.Length); } jsonWriter.WriteNumber("dnsKeyTtl", zoneInfo.DnsKeyTtl); jsonWriter.WritePropertyName("dnssecPrivateKeys"); jsonWriter.WriteStartArray(); IReadOnlyCollection dnssecPrivateKeys = zoneInfo.DnssecPrivateKeys; if (dnssecPrivateKeys is not null) { List sortedDnssecPrivateKey = new List(dnssecPrivateKeys); sortedDnssecPrivateKey.Sort(delegate (DnssecPrivateKey key1, DnssecPrivateKey key2) { int value = key1.KeyType.CompareTo(key2.KeyType); if (value == 0) value = key1.StateChangedOn.CompareTo(key2.StateChangedOn); return value; }); foreach (DnssecPrivateKey dnssecPrivateKey in sortedDnssecPrivateKey) WriteDnssecPrivateKeyAsJson(dnssecPrivateKey, jsonWriter); } jsonWriter.WriteEndArray(); } public void ConvertPrimaryZoneToNSEC(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.AuthZoneManager.ConvertPrimaryZoneToNSEC(zoneName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone was converted to NSEC successfully: " + zoneName); } public void ConvertPrimaryZoneToNSEC3(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); ushort iterations = request.GetQueryOrForm("iterations", ushort.Parse, 0); byte saltLength = request.GetQueryOrForm("saltLength", byte.Parse, 0); _dnsWebService._dnsServer.AuthZoneManager.ConvertPrimaryZoneToNSEC3(zoneName, iterations, saltLength); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone was converted to NSEC3 successfully: " + zoneName); } public void UpdatePrimaryZoneNSEC3Parameters(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); ushort iterations = request.GetQueryOrForm("iterations", ushort.Parse, 0); byte saltLength = request.GetQueryOrForm("saltLength", byte.Parse, 0); _dnsWebService._dnsServer.AuthZoneManager.UpdatePrimaryZoneNSEC3Parameters(zoneName, iterations, saltLength); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone NSEC3 parameters were updated successfully: " + zoneName); } public void UpdatePrimaryZoneDnssecDnsKeyTtl(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); uint dnsKeyTtl = request.GetQueryOrForm("ttl", ZoneFile.ParseTtl); _dnsWebService._dnsServer.AuthZoneManager.UpdatePrimaryZoneDnsKeyTtl(zoneName, dnsKeyTtl); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone DNSKEY TTL was updated successfully: " + zoneName); } public void AddPrimaryZoneDnssecPrivateKey(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); DnssecPrivateKeyType keyType = request.GetQueryOrFormEnum("keyType"); ushort rolloverDays = request.GetQueryOrForm("rolloverDays", ushort.Parse, (ushort)(keyType == DnssecPrivateKeyType.ZoneSigningKey ? 30 : 0)); string algorithm = request.GetQueryOrForm("algorithm"); string pemPrivateKey = request.GetQueryOrForm("pemPrivateKey", null); DnssecPrivateKey privateKey; switch (algorithm.ToUpper()) { case "RSA": { string hashAlgorithm = request.GetQueryOrForm("hashAlgorithm"); DnssecAlgorithm dnssecAlgorithm; switch (hashAlgorithm.ToUpper()) { case "MD5": dnssecAlgorithm = DnssecAlgorithm.RSAMD5; break; case "SHA1": dnssecAlgorithm = DnssecAlgorithm.RSASHA1; break; case "SHA256": dnssecAlgorithm = DnssecAlgorithm.RSASHA256; break; case "SHA512": dnssecAlgorithm = DnssecAlgorithm.RSASHA512; break; default: throw new NotSupportedException("Hash algorithm is not supported: " + hashAlgorithm); } if (pemPrivateKey is null) { int keySize = request.GetQueryOrForm("keySize", int.Parse); privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays, keySize); } else { privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey); privateKey.RolloverDays = rolloverDays; _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey); } } break; case "ECDSA": { string curve = request.GetQueryOrForm("curve"); DnssecAlgorithm dnssecAlgorithm; switch (curve.ToUpper()) { case "P256": dnssecAlgorithm = DnssecAlgorithm.ECDSAP256SHA256; break; case "P384": dnssecAlgorithm = DnssecAlgorithm.ECDSAP384SHA384; break; default: throw new NotSupportedException("ECDSA curve is not supported: " + curve); } if (pemPrivateKey is null) { privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays); } else { privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey); privateKey.RolloverDays = rolloverDays; _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey); } } break; case "EDDSA": { string curve = request.GetQueryOrForm("curve"); DnssecAlgorithm dnssecAlgorithm; switch (curve.ToUpper()) { case "ED25519": dnssecAlgorithm = DnssecAlgorithm.ED25519; break; case "ED448": dnssecAlgorithm = DnssecAlgorithm.ED448; break; default: throw new NotSupportedException("EdDSA curve is not supported: " + curve); } if (pemPrivateKey is null) { privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays); } else { privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey); privateKey.RolloverDays = rolloverDays; _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey); } } break; default: throw new NotSupportedException("Algorithm is not supported: " + algorithm); } Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("addedDnssecPrivateKey"); WriteDnssecPrivateKeyAsJson(privateKey, jsonWriter); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNSSEC private key was generated and added to the primary zone successfully: " + zoneName); } public void UpdatePrimaryZoneDnssecPrivateKey(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); ushort rolloverDays = request.GetQueryOrForm("rolloverDays", ushort.Parse); DnssecPrivateKey privateKey = _dnsWebService._dnsServer.AuthZoneManager.UpdatePrimaryZoneDnssecPrivateKey(zoneName, keyTag, rolloverDays); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("updatedDnssecPrivateKey"); WriteDnssecPrivateKeyAsJson(privateKey, jsonWriter); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Primary zone DNSSEC private key config was updated successfully: " + zoneName); } public void DeletePrimaryZoneDnssecPrivateKey(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); _dnsWebService._dnsServer.AuthZoneManager.DeletePrimaryZoneDnssecPrivateKey(zoneName, keyTag); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] DNSSEC private key was deleted from primary zone successfully: " + zoneName); } public void PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); _dnsWebService._dnsServer.AuthZoneManager.PublishAllGeneratedPrimaryZoneDnssecPrivateKeys(zoneName); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] All DNSSEC private keys from the primary zone were published successfully: " + zoneName); } public void RolloverPrimaryZoneDnsKey(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); _dnsWebService._dnsServer.AuthZoneManager.RolloverPrimaryZoneDnsKey(zoneName, keyTag); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] The DNSKEY (" + keyTag + ") from the primary zone was rolled over successfully: " + zoneName); } public async Task RetirePrimaryZoneDnsKeyAsync(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrForm("zone").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneName, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); await _dnsWebService._dnsServer.AuthZoneManager.RetirePrimaryZoneDnsKeyAsync(zoneName, keyTag); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] The DNSKEY (" + keyTag + ") from the primary zone was retired successfully: " + zoneName); } public void DeleteZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); switch (zoneInfo.Type) { case AuthZoneType.Primary: if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterPrimaryZone(zoneInfo.Name)) throw new DnsWebServiceException("Cannot delete the Cluster Primary zone '" + zoneInfo.DisplayName + "'."); break; case AuthZoneType.Catalog: if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.Name)) throw new DnsWebServiceException("Cannot delete the Cluster Catalog zone '" + zoneInfo.DisplayName + "'."); break; } if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteZone(zoneInfo, true)) throw new DnsWebServiceException("Failed to delete the zone '" + zoneInfo.DisplayName + "': no such zone exists."); _dnsWebService._authManager.RemoveAllPermissions(PermissionSection.Zones, zoneInfo.Name); _dnsWebService._authManager.SaveConfigFile(); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + zoneInfo.TypeName + " zone was deleted: " + zoneInfo.DisplayName); //delete cache for this zone to allow rebuilding cache data without using the current zone _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name); } public void EnableZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); zoneInfo.Disabled = false; _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + zoneInfo.TypeName + " zone was enabled: " + zoneInfo.DisplayName); //delete cache for this zone to allow rebuilding cache data as needed by stub or forwarder zone _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name); } public void DisableZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); switch (zoneInfo.Type) { case AuthZoneType.Primary: if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterPrimaryZone(zoneInfo.Name)) throw new DnsWebServiceException("Cannot disable the Cluster Primary zone '" + zoneInfo.DisplayName + "'."); break; case AuthZoneType.Catalog: if (_dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.Name)) throw new DnsWebServiceException("Cannot disable the Cluster Catalog zone '" + zoneInfo.DisplayName + "'."); break; } zoneInfo.Disabled = true; _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + zoneInfo.TypeName + " zone was disabled: " + zoneInfo.DisplayName); //delete cache for this zone to allow rebuilding cache data without using the current zone _dnsWebService._dnsServer.CacheZoneManager.DeleteZone(zoneInfo.Name); } public void GetZoneOptions(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrFormAlt("zone", "domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); bool includeAvailableCatalogZoneNames = request.GetQueryOrForm("includeAvailableCatalogZoneNames", bool.Parse, false); bool includeAvailableTsigKeyNames = request.GetQueryOrForm("includeAvailableTsigKeyNames", bool.Parse, false); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WriteString("name", zoneInfo.Name); if (DnsClient.TryConvertDomainNameToUnicode(zoneInfo.Name, out string nameIdn)) jsonWriter.WriteString("nameIdn", nameIdn); jsonWriter.WriteString("type", zoneInfo.Type.ToString()); if (zoneInfo.Type == AuthZoneType.Primary) jsonWriter.WriteBoolean("internal", zoneInfo.Internal); switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: jsonWriter.WriteString("dnssecStatus", zoneInfo.ApexZone.DnssecStatus.ToString()); break; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: if (!zoneInfo.Internal) { string[] notifyFailed = zoneInfo.NotifyFailed; jsonWriter.WriteBoolean("notifyFailed", notifyFailed.Length > 0); jsonWriter.WritePropertyName("notifyFailedFor"); jsonWriter.WriteStartArray(); foreach (string server in notifyFailed) jsonWriter.WriteStringValue(server); jsonWriter.WriteEndArray(); } break; } jsonWriter.WriteBoolean("disabled", zoneInfo.Disabled); //catalog zone switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: jsonWriter.WriteString("catalog", zoneInfo.CatalogZoneName); if (zoneInfo.CatalogZoneName is not null) { jsonWriter.WriteBoolean("overrideCatalogQueryAccess", zoneInfo.OverrideCatalogQueryAccess); jsonWriter.WriteBoolean("overrideCatalogZoneTransfer", zoneInfo.OverrideCatalogZoneTransfer); jsonWriter.WriteBoolean("overrideCatalogNotify", zoneInfo.OverrideCatalogNotify); } break; case AuthZoneType.Stub: jsonWriter.WriteString("catalog", zoneInfo.CatalogZoneName); if (zoneInfo.CatalogZoneName is not null) { jsonWriter.WriteBoolean("isSecondaryCatalogMember", zoneInfo.ApexZone.SecondaryCatalogZone is not null); jsonWriter.WriteBoolean("overrideCatalogQueryAccess", zoneInfo.OverrideCatalogQueryAccess); } break; case AuthZoneType.Secondary: jsonWriter.WriteString("catalog", zoneInfo.CatalogZoneName); if (zoneInfo.CatalogZoneName is not null) { jsonWriter.WriteBoolean("isSecondaryCatalogMember", zoneInfo.ApexZone.SecondaryCatalogZone is not null); jsonWriter.WriteBoolean("overrideCatalogQueryAccess", zoneInfo.OverrideCatalogQueryAccess); jsonWriter.WriteBoolean("overrideCatalogZoneTransfer", zoneInfo.OverrideCatalogZoneTransfer); jsonWriter.WriteBoolean("overrideCatalogPrimaryNameServers", zoneInfo.OverrideCatalogPrimaryNameServers); } break; case AuthZoneType.SecondaryForwarder: jsonWriter.WriteString("catalog", zoneInfo.CatalogZoneName); if (zoneInfo.CatalogZoneName is not null) jsonWriter.WriteBoolean("overrideCatalogQueryAccess", zoneInfo.OverrideCatalogQueryAccess); break; } //primary server switch (zoneInfo.Type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: case AuthZoneType.Stub: jsonWriter.WriteStartArray("primaryNameServerAddresses"); IReadOnlyList primaryNameServerAddresses = zoneInfo.PrimaryNameServerAddresses; if (primaryNameServerAddresses is not null) { foreach (NameServerAddress primaryNameServerAddress in primaryNameServerAddresses) jsonWriter.WriteStringValue(primaryNameServerAddress.OriginalAddress); } jsonWriter.WriteEndArray(); break; } switch (zoneInfo.Type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: if (zoneInfo.PrimaryZoneTransferProtocol == DnsTransportProtocol.Udp) jsonWriter.WriteString("primaryZoneTransferProtocol", "Tcp"); else jsonWriter.WriteString("primaryZoneTransferProtocol", zoneInfo.PrimaryZoneTransferProtocol.ToString()); jsonWriter.WriteString("primaryZoneTransferTsigKeyName", zoneInfo.PrimaryZoneTransferTsigKeyName); break; } if (zoneInfo.Type == AuthZoneType.Secondary) jsonWriter.WriteBoolean("validateZone", zoneInfo.ValidateZone); //query access { jsonWriter.WriteString("queryAccess", zoneInfo.QueryAccess.ToString()); jsonWriter.WriteStartArray("queryAccessNetworkACL"); if (zoneInfo.QueryAccessNetworkACL is not null) { foreach (NetworkAccessControl nac in zoneInfo.QueryAccessNetworkACL) jsonWriter.WriteStringValue(nac.ToString()); } jsonWriter.WriteEndArray(); } //zone transfer switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: case AuthZoneType.SecondaryCatalog: jsonWriter.WriteString("zoneTransfer", zoneInfo.ZoneTransfer.ToString()); jsonWriter.WritePropertyName("zoneTransferNetworkACL"); { jsonWriter.WriteStartArray(); if (zoneInfo.ZoneTransferNetworkACL is not null) { foreach (NetworkAccessControl nac in zoneInfo.ZoneTransferNetworkACL) jsonWriter.WriteStringValue(nac.ToString()); } jsonWriter.WriteEndArray(); } jsonWriter.WritePropertyName("zoneTransferTsigKeyNames"); { jsonWriter.WriteStartArray(); if (zoneInfo.ZoneTransferTsigKeyNames is not null) { foreach (string tsigKeyName in zoneInfo.ZoneTransferTsigKeyNames) jsonWriter.WriteStringValue(tsigKeyName); } jsonWriter.WriteEndArray(); } break; } //notify switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: jsonWriter.WriteString("notify", zoneInfo.Notify.ToString()); jsonWriter.WritePropertyName("notifyNameServers"); { jsonWriter.WriteStartArray(); if (zoneInfo.NotifyNameServers is not null) { foreach (IPAddress nameServer in zoneInfo.NotifyNameServers) jsonWriter.WriteStringValue(nameServer.ToString()); } jsonWriter.WriteEndArray(); } if (zoneInfo.Type == AuthZoneType.Catalog) { jsonWriter.WriteStartArray("notifySecondaryCatalogsNameServers"); if (zoneInfo.NotifySecondaryCatalogNameServers is not null) { foreach (IPAddress nameServer in zoneInfo.NotifySecondaryCatalogNameServers) jsonWriter.WriteStringValue(nameServer.ToString()); } jsonWriter.WriteEndArray(); } break; } //update switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.Forwarder: jsonWriter.WriteString("update", zoneInfo.Update.ToString()); jsonWriter.WritePropertyName("updateNetworkACL"); { jsonWriter.WriteStartArray(); if (zoneInfo.UpdateNetworkACL is not null) { foreach (NetworkAccessControl nac in zoneInfo.UpdateNetworkACL) jsonWriter.WriteStringValue(nac.ToString()); } jsonWriter.WriteEndArray(); } break; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: jsonWriter.WritePropertyName("updateSecurityPolicies"); { jsonWriter.WriteStartArray(); if (zoneInfo.UpdateSecurityPolicies is not null) { foreach (KeyValuePair>> updateSecurityPolicy in zoneInfo.UpdateSecurityPolicies) { foreach (KeyValuePair> policy in updateSecurityPolicy.Value) { jsonWriter.WriteStartObject(); jsonWriter.WriteString("tsigKeyName", updateSecurityPolicy.Key); jsonWriter.WriteString("domain", policy.Key); jsonWriter.WritePropertyName("allowedTypes"); jsonWriter.WriteStartArray(); foreach (DnsResourceRecordType allowedType in policy.Value) jsonWriter.WriteStringValue(allowedType.ToString()); jsonWriter.WriteEndArray(); jsonWriter.WriteEndObject(); } } } jsonWriter.WriteEndArray(); } break; } if (includeAvailableCatalogZoneNames) { IReadOnlyList catalogZoneInfoList = _dnsWebService._dnsServer.AuthZoneManager.GetCatalogZones(delegate (AuthZoneInfo catalogZoneInfo) { return !catalogZoneInfo.Disabled && _dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify); }); jsonWriter.WritePropertyName("availableCatalogZoneNames"); jsonWriter.WriteStartArray(); foreach (AuthZoneInfo catalogZoneInfo in catalogZoneInfoList) jsonWriter.WriteStringValue(catalogZoneInfo.Name); jsonWriter.WriteEndArray(); } if (includeAvailableTsigKeyNames) { jsonWriter.WritePropertyName("availableTsigKeyNames"); { jsonWriter.WriteStartArray(); if (_dnsWebService._dnsServer.TsigKeys is not null) { foreach (KeyValuePair tsigKey in _dnsWebService._dnsServer.TsigKeys) jsonWriter.WriteStringValue(tsigKey.Key); } jsonWriter.WriteEndArray(); } } } public void SetZoneOptions(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); HttpRequest request = context.Request; string zoneName = request.GetQueryOrFormAlt("zone", "domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); if (request.TryGetQueryOrForm("disabled", bool.Parse, out bool disabled)) zoneInfo.Disabled = disabled; //catalog zone override options switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: { if (request.TryGetQueryOrForm("overrideCatalogQueryAccess", bool.Parse, out bool overrideCatalogQueryAccess)) zoneInfo.OverrideCatalogQueryAccess = overrideCatalogQueryAccess; if (request.TryGetQueryOrForm("overrideCatalogZoneTransfer", bool.Parse, out bool overrideCatalogZoneTransfer)) zoneInfo.OverrideCatalogZoneTransfer = overrideCatalogZoneTransfer; if (request.TryGetQueryOrForm("overrideCatalogNotify", bool.Parse, out bool overrideCatalogNotify)) zoneInfo.OverrideCatalogNotify = overrideCatalogNotify; } break; case AuthZoneType.Secondary: { if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for Secondary zone that is a member of Secondary Catalog Zone if (request.TryGetQueryOrForm("overrideCatalogQueryAccess", bool.Parse, out bool overrideCatalogQueryAccess)) zoneInfo.OverrideCatalogQueryAccess = overrideCatalogQueryAccess; if (request.TryGetQueryOrForm("overrideCatalogZoneTransfer", bool.Parse, out bool overrideCatalogZoneTransfer)) zoneInfo.OverrideCatalogZoneTransfer = overrideCatalogZoneTransfer; } break; case AuthZoneType.Stub: { if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for Stub zone that is a member of Secondary Catalog Zone if (request.TryGetQueryOrForm("overrideCatalogQueryAccess", bool.Parse, out bool overrideCatalogQueryAccess)) zoneInfo.OverrideCatalogQueryAccess = overrideCatalogQueryAccess; } break; } //primary server switch (zoneInfo.Type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: { if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for zone that is a member of Secondary Catalog Zone if (request.TryGetQueryOrFormEnum("primaryZoneTransferProtocol", out DnsTransportProtocol primaryZoneTransferProtocol)) { if (primaryZoneTransferProtocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); zoneInfo.PrimaryZoneTransferProtocol = primaryZoneTransferProtocol; } string primaryNameServerAddresses = request.QueryOrForm("primaryNameServerAddresses"); if (primaryNameServerAddresses is not null) { if (primaryNameServerAddresses.Length == 0) { zoneInfo.PrimaryNameServerAddresses = null; } else { zoneInfo.PrimaryNameServerAddresses = primaryNameServerAddresses.Split(delegate (string address) { NameServerAddress nameServer = NameServerAddress.Parse(address); if (nameServer.Protocol != primaryZoneTransferProtocol) nameServer = nameServer.Clone(primaryZoneTransferProtocol); return nameServer; }, ','); } } string primaryZoneTransferTsigKeyName = request.QueryOrForm("primaryZoneTransferTsigKeyName"); if (primaryZoneTransferTsigKeyName is not null) { if (primaryZoneTransferTsigKeyName.Length == 0) zoneInfo.PrimaryZoneTransferTsigKeyName = null; else zoneInfo.PrimaryZoneTransferTsigKeyName = primaryZoneTransferTsigKeyName; } } break; case AuthZoneType.Stub: { if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for Stub zone that is a member of Secondary Catalog Zone string primaryNameServerAddresses = request.QueryOrForm("primaryNameServerAddresses"); if (primaryNameServerAddresses is not null) { if (primaryNameServerAddresses.Length == 0) { zoneInfo.PrimaryNameServerAddresses = null; } else { zoneInfo.PrimaryNameServerAddresses = primaryNameServerAddresses.Split(delegate (string address) { NameServerAddress nameServer = NameServerAddress.Parse(address); if (nameServer.Protocol != DnsTransportProtocol.Udp) nameServer = nameServer.Clone(DnsTransportProtocol.Udp); return nameServer; }, ','); } } } break; } if (zoneInfo.Type == AuthZoneType.Secondary) { if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) { //cannot set option for zone that is a member of Secondary Catalog Zone } else if (request.TryGetQueryOrForm("validateZone", bool.Parse, out bool validateZone)) { zoneInfo.ValidateZone = validateZone; } } //query access switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: case AuthZoneType.SecondaryForwarder: case AuthZoneType.Catalog: if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for zone that is a member of Secondary Catalog Zone string queryAccessNetworkACL = request.QueryOrForm("queryAccessNetworkACL"); if (queryAccessNetworkACL is not null) { if ((queryAccessNetworkACL.Length == 0) || queryAccessNetworkACL.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.QueryAccessNetworkACL = null; else zoneInfo.QueryAccessNetworkACL = queryAccessNetworkACL.Split(NetworkAccessControl.Parse, ','); } if (request.TryGetQueryOrFormEnum("queryAccess", out AuthZoneQueryAccess queryAccess)) zoneInfo.QueryAccess = queryAccess; break; } //zone transfer switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for zone that is a member of Secondary Catalog Zone string strZoneTransferNetworkACL = request.QueryOrForm("zoneTransferNetworkACL"); if (strZoneTransferNetworkACL is not null) { if ((strZoneTransferNetworkACL.Length == 0) || strZoneTransferNetworkACL.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.ZoneTransferNetworkACL = null; else zoneInfo.ZoneTransferNetworkACL = strZoneTransferNetworkACL.Split(NetworkAccessControl.Parse, ','); } if (request.TryGetQueryOrFormEnum("zoneTransfer", out AuthZoneTransfer zoneTransfer)) zoneInfo.ZoneTransfer = zoneTransfer; string strZoneTransferTsigKeyNames = request.QueryOrForm("zoneTransferTsigKeyNames"); if (strZoneTransferTsigKeyNames is not null) { if ((strZoneTransferTsigKeyNames.Length == 0) || strZoneTransferTsigKeyNames.Equals("false", StringComparison.OrdinalIgnoreCase)) { zoneInfo.ZoneTransferTsigKeyNames = null; } else { string[] strZoneTransferTsigKeyNamesParts = strZoneTransferTsigKeyNames.Split(_commaSeparator, StringSplitOptions.RemoveEmptyEntries); HashSet zoneTransferTsigKeyNames = new HashSet(strZoneTransferTsigKeyNamesParts.Length); for (int i = 0; i < strZoneTransferTsigKeyNamesParts.Length; i++) zoneTransferTsigKeyNames.Add(strZoneTransferTsigKeyNamesParts[i].Trim('.').ToLowerInvariant()); zoneInfo.ZoneTransferTsigKeyNames = zoneTransferTsigKeyNames; } } break; } //notify switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: if (request.TryGetQueryOrFormEnum("notify", out AuthZoneNotify notify)) zoneInfo.Notify = notify; string strNotifyNameServers = request.QueryOrForm("notifyNameServers"); if (strNotifyNameServers is not null) { if ((strNotifyNameServers.Length == 0) || strNotifyNameServers.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.NotifyNameServers = null; else zoneInfo.NotifyNameServers = strNotifyNameServers.Split(IPAddress.Parse, ','); } if (zoneInfo.Type == AuthZoneType.Catalog) { string strNotifySecondaryCatalogNameServers = request.QueryOrForm("notifySecondaryCatalogsNameServers"); if (strNotifySecondaryCatalogNameServers is not null) { if ((strNotifySecondaryCatalogNameServers.Length == 0) || strNotifySecondaryCatalogNameServers.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.NotifySecondaryCatalogNameServers = null; else zoneInfo.NotifySecondaryCatalogNameServers = strNotifySecondaryCatalogNameServers.Split(IPAddress.Parse, ','); } } break; } //update switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.Forwarder: if (request.TryGetQueryOrFormEnum("update", out AuthZoneUpdate update)) zoneInfo.Update = update; string strUpdateNetworkACL = request.QueryOrForm("updateNetworkACL"); if (strUpdateNetworkACL is not null) { if ((strUpdateNetworkACL.Length == 0) || strUpdateNetworkACL.Equals("false", StringComparison.OrdinalIgnoreCase)) zoneInfo.UpdateNetworkACL = null; else zoneInfo.UpdateNetworkACL = strUpdateNetworkACL.Split(NetworkAccessControl.Parse, ','); } break; } switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: string strUpdateSecurityPolicies = request.QueryOrForm("updateSecurityPolicies"); if (strUpdateSecurityPolicies is not null) { if ((strUpdateSecurityPolicies.Length == 0) || strUpdateSecurityPolicies.Equals("false", StringComparison.OrdinalIgnoreCase)) { zoneInfo.UpdateSecurityPolicies = null; } else { string[] strUpdateSecurityPoliciesParts = strUpdateSecurityPolicies.Split(_pipeSeparator, StringSplitOptions.RemoveEmptyEntries); Dictionary>> updateSecurityPolicies = new Dictionary>>(strUpdateSecurityPoliciesParts.Length); for (int i = 0; i < strUpdateSecurityPoliciesParts.Length; i += 3) { string tsigKeyName = strUpdateSecurityPoliciesParts[i].Trim('.').ToLowerInvariant(); string domain = strUpdateSecurityPoliciesParts[i + 1].Trim('.').ToLowerInvariant(); string strTypes = strUpdateSecurityPoliciesParts[i + 2]; if (!domain.Equals(zoneInfo.Name, StringComparison.OrdinalIgnoreCase) && !domain.EndsWith("." + zoneInfo.Name, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Cannot set Dynamic Updates security policies: the domain '" + domain + "' must be part of the current zone."); if (!updateSecurityPolicies.TryGetValue(tsigKeyName, out IReadOnlyDictionary> policyMap)) { policyMap = new Dictionary>(); updateSecurityPolicies.Add(tsigKeyName, policyMap); } if (!policyMap.TryGetValue(domain, out IReadOnlyList types)) { types = new List(); (policyMap as Dictionary>).Add(domain, types); } foreach (string strType in strTypes.Split(_commaSpaceSeparator, StringSplitOptions.RemoveEmptyEntries)) (types as List).Add(Enum.Parse(strType, true)); } zoneInfo.UpdateSecurityPolicies = updateSecurityPolicies; } } break; } //catalog zone; done last to allow using updated properties when there is change of ownership switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Secondary: case AuthZoneType.Stub: case AuthZoneType.Forwarder: if (zoneInfo.ApexZone.SecondaryCatalogZone is not null) break; //cannot set option for Stub zone that is a member of Secondary Catalog Zone string catalogZoneName = request.QueryOrForm("catalog"); if (catalogZoneName is not null) { string oldCatalogZoneName = zoneInfo.CatalogZoneName; if (catalogZoneName.Length == 0) { if (!string.IsNullOrEmpty(oldCatalogZoneName)) _dnsWebService._dnsServer.AuthZoneManager.RemoveCatalogMemberZone(zoneInfo); } else { if (string.IsNullOrEmpty(oldCatalogZoneName)) { //check catalog permissions AuthZoneInfo catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName); if (catalogZoneInfo is null) throw new DnsWebServiceException("No such Catalog zone was found: " + catalogZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied to use Catalog zone: " + catalogZoneInfo.Name); _dnsWebService._dnsServer.AuthZoneManager.AddCatalogMemberZone(catalogZoneInfo.Name, zoneInfo); if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneInfo.Name)) _dnsWebService._clusterManager.UpdateClusterRecordsFor(zoneInfo); } else if (!catalogZoneName.Equals(oldCatalogZoneName, StringComparison.OrdinalIgnoreCase)) { //check catalog permissions AuthZoneInfo catalogZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(catalogZoneName); if (catalogZoneInfo is null) throw new DnsWebServiceException("No such Catalog zone was found: " + catalogZoneName); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, catalogZoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied to use Catalog zone: " + catalogZoneInfo.Name); _dnsWebService._dnsServer.AuthZoneManager.ChangeCatalogMemberZoneOwnership(zoneInfo, catalogZoneInfo.Name); if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(catalogZoneInfo.Name)) _dnsWebService._clusterManager.UpdateClusterRecordsFor(zoneInfo); } } } if (zoneInfo.ApexZone.CatalogZone is not null) _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.ApexZone.CatalogZoneName); break; } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] " + zoneInfo.TypeName + " zone options were updated successfully: " + zoneInfo.DisplayName); _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } public void ResyncZone(HttpContext context) { User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string zoneName = context.Request.GetQueryOrFormAlt("zone", "domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.GetAuthZoneInfo(zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + zoneName); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); switch (zoneInfo.Type) { case AuthZoneType.Secondary: case AuthZoneType.SecondaryForwarder: case AuthZoneType.SecondaryCatalog: case AuthZoneType.Stub: zoneInfo.TriggerResync(); break; default: throw new DnsWebServiceException("Only Secondary, Secondary Forwarder, Secondary Catalog, and Stub zones support resync."); } } public void AddRecord(HttpContext context) { HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) { zoneName = zoneName.Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); } AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + domain); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); uint defaultTtl; switch (type) { case DnsResourceRecordType.NS: defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl; break; default: defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl; break; } uint ttl = request.GetQueryOrForm("ttl", ZoneFile.ParseTtl, defaultTtl); bool overwrite = request.GetQueryOrForm("overwrite", bool.Parse, false); string comments = request.QueryOrForm("comments"); uint expiryTtl = request.GetQueryOrForm("expiryTtl", ZoneFile.ParseTtl, 0u); DnsResourceRecord newRecord; switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { string strIPAddress = request.GetQueryOrFormAlt("ipAddress", "value"); IPAddress ipAddress; if (strIPAddress.Equals("request-ip-address")) ipAddress = context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader).Address; else ipAddress = IPAddress.Parse(strIPAddress); bool ptr = request.GetQueryOrForm("ptr", bool.Parse, false); if (ptr) { string ptrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128); AuthZoneInfo reverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain); if (reverseZoneInfo is null) { bool createPtrZone = request.GetQueryOrForm("createPtrZone", bool.Parse, false); if (!createPtrZone) throw new DnsWebServiceException("No reverse zone available to add PTR record."); string ptrZone = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 24 : 64); reverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(ptrZone); if (reverseZoneInfo == null) throw new DnsWebServiceException("Failed to create reverse zone to add PTR record: " + ptrZone); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, reverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); } if (reverseZoneInfo.Internal) throw new DnsWebServiceException("Reverse zone '" + reverseZoneInfo.DisplayName + "' is an internal zone."); if ((reverseZoneInfo.Type != AuthZoneType.Primary) && (reverseZoneInfo.Type != AuthZoneType.Forwarder)) throw new DnsWebServiceException("Reverse zone '" + reverseZoneInfo.DisplayName + "' is not a primary or forwarder zone."); DnsResourceRecord ptrRecord = new DnsResourceRecord(ptrDomain, DnsResourceRecordType.PTR, DnsClass.IN, ttl, new DnsPTRRecordData(domain)); ptrRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; ptrRecord.GetAuthGenericRecordInfo().ExpiryTtl = expiryTtl; _dnsWebService._dnsServer.AuthZoneManager.SetRecord(reverseZoneInfo.Name, ptrRecord); _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name); } if (type == DnsResourceRecordType.A) newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsARecordData(ipAddress)); else newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsAAAARecordData(ipAddress)); } break; case DnsResourceRecordType.NS: { if ((zoneInfo.Type == AuthZoneType.Primary) && zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName)) throw new DnsWebServiceException("Cannot add NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); string nameServer = request.GetQueryOrFormAlt("nameServer", "value").Trim('.'); string glueAddresses = request.GetQueryOrForm("glue", null); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsNSRecordData(nameServer)); if (!string.IsNullOrEmpty(glueAddresses)) { if (zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && (nameServer.Equals(domain, StringComparison.OrdinalIgnoreCase) || nameServer.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase))) throw new DnsWebServiceException("The zone's own NS records cannot have glue addresses. Please add separate A/AAAA records in the zone instead."); newRecord.SetGlueRecords(glueAddresses); } } break; case DnsResourceRecordType.CNAME: { if (!overwrite) { IReadOnlyList existingRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneInfo.Name, domain, type); if (existingRecords.Count > 0) throw new DnsWebServiceException("Record already exists. Use overwrite option if you wish to overwrite existing record."); } string cname = request.GetQueryOrFormAlt("cname", "value").Trim('.'); if (cname.Equals(domain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("CNAME domain name cannot be same as that of the record name."); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname)); overwrite = true; //force SetRecord } break; case DnsResourceRecordType.PTR: { string ptrName = request.GetQueryOrFormAlt("ptrName", "value").Trim('.'); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsPTRRecordData(ptrName)); } break; case DnsResourceRecordType.MX: { ushort preference = request.GetQueryOrForm("preference", ushort.Parse); string exchange = request.GetQueryOrFormAlt("exchange", "value").Trim('.'); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsMXRecordData(preference, exchange)); } break; case DnsResourceRecordType.TXT: { string text = request.GetQueryOrFormAlt("text", "value"); bool splitText = request.GetQueryOrForm("splitText", bool.Parse, false); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, splitText ? new DnsTXTRecordData(DecodeCharacterStrings(text)) : new DnsTXTRecordData(text)); } break; case DnsResourceRecordType.RP: { string mailbox = request.GetQueryOrForm("mailbox", "").Trim('.'); string txtDomain = request.GetQueryOrForm("txtDomain", "").Trim('.'); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsRPRecordData(mailbox, txtDomain)); } break; case DnsResourceRecordType.SRV: { ushort priority = request.GetQueryOrForm("priority", ushort.Parse); ushort weight = request.GetQueryOrForm("weight", ushort.Parse); ushort port = request.GetQueryOrForm("port", ushort.Parse); string target = request.GetQueryOrFormAlt("target", "value").Trim('.'); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSRVRecordData(priority, weight, port, target)); } break; case DnsResourceRecordType.NAPTR: { ushort order = request.GetQueryOrForm("naptrOrder", ushort.Parse); ushort preference = request.GetQueryOrForm("naptrPreference", ushort.Parse); string flags = request.GetQueryOrForm("naptrFlags", ""); string services = request.GetQueryOrForm("naptrServices", ""); string regexp = request.GetQueryOrForm("naptrRegexp", ""); string replacement = request.GetQueryOrForm("naptrReplacement", "").Trim('.'); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsNAPTRRecordData(order, preference, flags, services, regexp, replacement)); } break; case DnsResourceRecordType.DNAME: { if (!overwrite) { IReadOnlyList existingRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneInfo.Name, domain, type); if (existingRecords.Count > 0) throw new DnsWebServiceException("Record already exists. Use overwrite option if you wish to overwrite existing record."); } string dname = request.GetQueryOrFormAlt("dname", "value").Trim('.'); if (dname.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("DNAME domain name cannot be a sub domain of the record name."); if (dname.Equals(domain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("DNAME domain name cannot be same as that of the record name."); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname)); overwrite = true; //force SetRecord } break; case DnsResourceRecordType.DS: { ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); DnssecAlgorithm algorithm = Enum.Parse(request.GetQueryOrForm("algorithm").Replace('-', '_'), true); DnssecDigestType digestType = Enum.Parse(request.GetQueryOrForm("digestType").Replace('-', '_'), true); byte[] digest = request.GetQueryOrFormAlt("digest", "value", Convert.FromHexString); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsDSRecordData(keyTag, algorithm, digestType, digest)); } break; case DnsResourceRecordType.SSHFP: { DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum("sshfpAlgorithm"); DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum("sshfpFingerprintType"); byte[] sshfpFingerprint = request.GetQueryOrForm("sshfpFingerprint", Convert.FromHexString); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint)); } break; case DnsResourceRecordType.TLSA: { DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("tlsaCertificateUsage").Replace('-', '_'), true); DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum("tlsaSelector"); DnsTLSAMatchingType tlsaMatchingType = Enum.Parse(request.GetQueryOrForm("tlsaMatchingType").Replace('-', '_'), true); string tlsaCertificateAssociationData = request.GetQueryOrForm("tlsaCertificateAssociationData"); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData)); } break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: { ushort svcPriority = request.GetQueryOrForm("svcPriority", ushort.Parse); string targetName = request.GetQueryOrForm("svcTargetName").Trim('.'); string strSvcParams = request.GetQueryOrForm("svcParams"); bool autoIpv4Hint = request.GetQueryOrForm("autoIpv4Hint", bool.Parse, false); bool autoIpv6Hint = request.GetQueryOrForm("autoIpv6Hint", bool.Parse, false); Dictionary svcParams; if (strSvcParams.Equals("false", StringComparison.OrdinalIgnoreCase)) { svcParams = new Dictionary(0); } else { string[] strSvcParamsParts = strSvcParams.Split('|'); svcParams = new Dictionary(strSvcParamsParts.Length / 2); for (int i = 0; i < strSvcParamsParts.Length; i += 2) { DnsSvcParamKey svcParamKey = Enum.Parse(strSvcParamsParts[i].Replace('-', '_'), true); DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]); svcParams.Add(svcParamKey, svcParamValue); } } newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSVCBRecordData(svcPriority, targetName, svcParams)); if (autoIpv4Hint) newRecord.GetAuthSVCBRecordInfo().AutoIpv4Hint = true; if (autoIpv6Hint) newRecord.GetAuthSVCBRecordInfo().AutoIpv6Hint = true; if (autoIpv4Hint || autoIpv6Hint) ResolveSvcbAutoHints(zoneInfo.Name, newRecord, autoIpv4Hint, autoIpv6Hint, svcParams); } break; case DnsResourceRecordType.URI: { ushort priority = request.GetQueryOrForm("uriPriority", ushort.Parse); ushort weight = request.GetQueryOrForm("uriWeight", ushort.Parse); Uri uri = request.GetQueryOrForm("uri", delegate (string value) { return new Uri(value); }); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsURIRecordData(priority, weight, uri)); } break; case DnsResourceRecordType.CAA: { byte flags = request.GetQueryOrForm("flags", byte.Parse); string tag = request.GetQueryOrForm("tag"); string value = request.GetQueryOrForm("value"); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsCAARecordData(flags, tag, value)); } break; case DnsResourceRecordType.ANAME: { string aname = request.GetQueryOrFormAlt("aname", "value").Trim('.'); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsANAMERecordData(aname)); } break; case DnsResourceRecordType.FWD: { DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); string forwarder = request.GetQueryOrFormAlt("forwarder", "value"); bool dnssecValidation = request.GetQueryOrForm("dnssecValidation", bool.Parse, false); DnsForwarderRecordProxyType proxyType = DnsForwarderRecordProxyType.DefaultProxy; string proxyAddress = null; ushort proxyPort = 0; string proxyUsername = null; string proxyPassword = null; if (!forwarder.Equals("this-server")) { proxyType = request.GetQueryOrFormEnum("proxyType", DnsForwarderRecordProxyType.DefaultProxy); switch (proxyType) { case DnsForwarderRecordProxyType.Http: case DnsForwarderRecordProxyType.Socks5: proxyAddress = request.GetQueryOrForm("proxyAddress"); proxyPort = request.GetQueryOrForm("proxyPort", ushort.Parse); proxyUsername = request.QueryOrForm("proxyUsername"); proxyPassword = request.QueryOrForm("proxyPassword"); break; } } byte priority = request.GetQueryOrForm("forwarderPriority", byte.Parse, byte.MinValue); if (protocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsForwarderRecordData(protocol, forwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, priority)); } break; case DnsResourceRecordType.APP: { if (!overwrite) { IReadOnlyList existingRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(zoneInfo.Name, domain, type); if (existingRecords.Count > 0) throw new DnsWebServiceException("Record already exists. Use overwrite option if you wish to overwrite existing record."); } string appName = request.GetQueryOrFormAlt("appName", "value"); string classPath = request.GetQueryOrForm("classPath"); string recordData = request.GetQueryOrForm("recordData", ""); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsApplicationRecordData(appName, classPath, recordData)); overwrite = true; //force SetRecord } break; default: { string strRData = request.GetQueryOrForm("rdata"); byte[] rdata; if (strRData.Contains(':')) rdata = strRData.ParseColonHexString(); else rdata = Convert.FromHexString(strRData); newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, DnsResourceRecord.ReadRecordDataFrom(type, rdata)); } break; } //update record info GenericRecordInfo recordInfo = newRecord.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.ExpiryTtl = expiryTtl; if (!string.IsNullOrEmpty(comments)) recordInfo.Comments = comments; //add record if (overwrite) { _dnsWebService._dnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); } else { if (!_dnsWebService._dnsServer.AuthZoneManager.AddRecord(zoneInfo.Name, newRecord)) throw new DnsWebServiceException("Cannot add record: record already exists."); } //additional processing if ((type == DnsResourceRecordType.A) || (type == DnsResourceRecordType.AAAA)) { bool updateSvcbHints = request.GetQueryOrForm("updateSvcbHints", bool.Parse, false); if (updateSvcbHints) UpdateSvcbAutoHints(zoneInfo.Name, domain, type == DnsResourceRecordType.A, type == DnsResourceRecordType.AAAA); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] New record was added to " + zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' successfully {record: " + newRecord.ToString() + "}"); //save zone _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("zone"); WriteZoneInfoAsJson(zoneInfo, jsonWriter); jsonWriter.WritePropertyName("addedRecord"); WriteRecordAsJson(newRecord, jsonWriter, true, null); } public void GetRecords(HttpContext context) { HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) { zoneName = zoneName.Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); } AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + domain); User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.View)) throw new DnsWebServiceException("Access was denied."); bool listZone = request.GetQueryOrForm("listZone", bool.Parse, false); List records = new List(); if (listZone) _dnsWebService._dnsServer.AuthZoneManager.ListAllZoneRecords(zoneInfo.Name, records); else _dnsWebService._dnsServer.AuthZoneManager.ListAllRecords(zoneInfo.Name, domain, records); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("zone"); WriteZoneInfoAsJson(zoneInfo, jsonWriter); WriteRecordsAsJson(records, jsonWriter, true, zoneInfo); } public void DeleteRecord(HttpContext context) { HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) { zoneName = zoneName.Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); } AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + domain); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Delete)) throw new DnsWebServiceException("Access was denied."); DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { IPAddress ipAddress = IPAddress.Parse(request.GetQueryOrFormAlt("ipAddress", "value")); if (type == DnsResourceRecordType.A) { if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsARecordData(ipAddress))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } else { if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsAAAARecordData(ipAddress))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } string ptrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128); AuthZoneInfo reverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(ptrDomain); if ((reverseZoneInfo is not null) && !reverseZoneInfo.Internal && ((reverseZoneInfo.Type == AuthZoneType.Primary) || (reverseZoneInfo.Type == AuthZoneType.Forwarder))) { IReadOnlyList ptrRecords = _dnsWebService._dnsServer.AuthZoneManager.GetRecords(reverseZoneInfo.Name, ptrDomain, DnsResourceRecordType.PTR); if (ptrRecords.Count > 0) { foreach (DnsResourceRecord ptrRecord in ptrRecords) { if ((ptrRecord.RDATA as DnsPTRRecordData).Domain.Equals(domain, StringComparison.OrdinalIgnoreCase)) { //delete PTR record and save reverse zone _dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(reverseZoneInfo.Name, ptrDomain, DnsResourceRecordType.PTR, ptrRecord.RDATA); _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(reverseZoneInfo.Name); break; } } } } bool updateSvcbHints = request.GetQueryOrForm("updateSvcbHints", bool.Parse, false); if (updateSvcbHints) UpdateSvcbAutoHints(zoneInfo.Name, domain, type == DnsResourceRecordType.A, type == DnsResourceRecordType.AAAA); } break; case DnsResourceRecordType.NS: { if ((zoneInfo.Type == AuthZoneType.Primary) && zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName)) throw new DnsWebServiceException("Cannot delete NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); string nameServer = request.GetQueryOrFormAlt("nameServer", "value").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsNSRecordData(nameServer, false))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.CNAME: if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, domain, type)) throw new DnsWebServiceException("Cannot delete record: no such record exists."); break; case DnsResourceRecordType.PTR: { string ptrName = request.GetQueryOrFormAlt("ptrName", "value").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsPTRRecordData(ptrName))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.MX: { ushort preference = request.GetQueryOrForm("preference", ushort.Parse); string exchange = request.GetQueryOrFormAlt("exchange", "value").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsMXRecordData(preference, exchange))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.TXT: { string text = request.GetQueryOrFormAlt("text", "value"); bool splitText = request.GetQueryOrForm("splitText", bool.Parse, false); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, splitText ? new DnsTXTRecordData(DecodeCharacterStrings(text)) : new DnsTXTRecordData(text))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.RP: { string mailbox = request.GetQueryOrForm("mailbox", "").Trim('.'); string txtDomain = request.GetQueryOrForm("txtDomain", "").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsRPRecordData(mailbox, txtDomain))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.SRV: { ushort priority = request.GetQueryOrForm("priority", ushort.Parse); ushort weight = request.GetQueryOrForm("weight", ushort.Parse); ushort port = request.GetQueryOrForm("port", ushort.Parse); string target = request.GetQueryOrFormAlt("target", "value").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSRVRecordData(priority, weight, port, target))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.NAPTR: { ushort order = request.GetQueryOrForm("naptrOrder", ushort.Parse); ushort preference = request.GetQueryOrForm("naptrPreference", ushort.Parse); string flags = request.GetQueryOrForm("naptrFlags", ""); string services = request.GetQueryOrForm("naptrServices", ""); string regexp = request.GetQueryOrForm("naptrRegexp", ""); string replacement = request.GetQueryOrForm("naptrReplacement", "").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsNAPTRRecordData(order, preference, flags, services, regexp, replacement))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.DNAME: if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, domain, type)) throw new DnsWebServiceException("Cannot delete record: no such record exists."); break; case DnsResourceRecordType.DS: { ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); DnssecAlgorithm algorithm = Enum.Parse(request.GetQueryOrForm("algorithm").Replace('-', '_'), true); DnssecDigestType digestType = Enum.Parse(request.GetQueryOrForm("digestType").Replace('-', '_'), true); byte[] digest = Convert.FromHexString(request.GetQueryOrFormAlt("digest", "value")); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsDSRecordData(keyTag, algorithm, digestType, digest))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.SSHFP: { DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum("sshfpAlgorithm"); DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum("sshfpFingerprintType"); byte[] sshfpFingerprint = request.GetQueryOrForm("sshfpFingerprint", Convert.FromHexString); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.TLSA: { DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("tlsaCertificateUsage").Replace('-', '_'), true); DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum("tlsaSelector"); DnsTLSAMatchingType tlsaMatchingType = Enum.Parse(request.GetQueryOrForm("tlsaMatchingType").Replace('-', '_'), true); string tlsaCertificateAssociationData = request.GetQueryOrForm("tlsaCertificateAssociationData"); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: { ushort svcPriority = request.GetQueryOrForm("svcPriority", ushort.Parse); string targetName = request.GetQueryOrForm("svcTargetName").Trim('.'); string strSvcParams = request.GetQueryOrForm("svcParams"); Dictionary svcParams; if (strSvcParams.Equals("false", StringComparison.OrdinalIgnoreCase)) { svcParams = new Dictionary(0); } else { string[] strSvcParamsParts = strSvcParams.Split('|'); svcParams = new Dictionary(strSvcParamsParts.Length / 2); for (int i = 0; i < strSvcParamsParts.Length; i += 2) { DnsSvcParamKey svcParamKey = Enum.Parse(strSvcParamsParts[i].Replace('-', '_'), true); DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]); svcParams.Add(svcParamKey, svcParamValue); } } if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsSVCBRecordData(svcPriority, targetName, svcParams))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.URI: { ushort priority = request.GetQueryOrForm("uriPriority", ushort.Parse); ushort weight = request.GetQueryOrForm("uriWeight", ushort.Parse); Uri uri = request.GetQueryOrForm("uri", delegate (string value) { return new Uri(value); }); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsURIRecordData(priority, weight, uri))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.CAA: { byte flags = request.GetQueryOrForm("flags", byte.Parse); string tag = request.GetQueryOrForm("tag"); string value = request.GetQueryOrForm("value"); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsCAARecordData(flags, tag, value))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.ANAME: { string aname = request.GetQueryOrFormAlt("aname", "value").Trim('.'); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsANAMERecordData(aname))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.FWD: { DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); string forwarder = request.GetQueryOrFormAlt("forwarder", "value"); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, DnsForwarderRecordData.CreatePartialRecordData(protocol, forwarder))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; case DnsResourceRecordType.APP: if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(zoneInfo.Name, domain, type)) throw new DnsWebServiceException("Cannot delete record: no such record exists."); break; default: { string strRData = request.GetQueryOrForm("rdata", string.Empty); byte[] rdata; if (strRData.Contains(':')) rdata = strRData.ParseColonHexString(); else rdata = Convert.FromHexString(strRData); if (!_dnsWebService._dnsServer.AuthZoneManager.DeleteRecord(zoneInfo.Name, domain, type, new DnsUnknownRecordData(rdata))) throw new DnsWebServiceException("Cannot delete record: no such record exists."); } break; } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Record was deleted from " + zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' successfully {domain: " + domain + "; type: " + type + ";}"); _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); } public void UpdateRecord(HttpContext context) { HttpRequest request = context.Request; string domain = request.GetQueryOrForm("domain").Trim('.'); if (DnsClient.IsDomainNameUnicode(domain)) domain = DnsClient.ConvertDomainNameToAscii(domain); string zoneName = request.QueryOrForm("zone"); if (zoneName is not null) { zoneName = zoneName.Trim('.'); if (DnsClient.IsDomainNameUnicode(zoneName)) zoneName = DnsClient.ConvertDomainNameToAscii(zoneName); } AuthZoneInfo zoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(string.IsNullOrEmpty(zoneName) ? domain : zoneName); if (zoneInfo is null) throw new DnsWebServiceException("No such zone was found: " + domain); if (zoneInfo.Internal) throw new DnsWebServiceException("Access was denied to manage internal DNS Server zone."); User sessionUser = _dnsWebService.GetSessionUser(context); if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Zones, zoneInfo.Name, sessionUser, PermissionFlag.Modify)) throw new DnsWebServiceException("Access was denied."); string newDomain = request.GetQueryOrForm("newDomain", domain).Trim('.'); DnsResourceRecordType type = request.GetQueryOrFormEnum("type"); uint defaultTtl; switch (type) { case DnsResourceRecordType.NS: defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultNsRecordTtl; break; case DnsResourceRecordType.SOA: defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultSoaRecordTtl; break; default: defaultTtl = _dnsWebService._dnsServer.AuthZoneManager.DefaultRecordTtl; break; } uint ttl = request.GetQueryOrForm("ttl", ZoneFile.ParseTtl, defaultTtl); bool disable = request.GetQueryOrForm("disable", bool.Parse, false); string comments = request.QueryOrForm("comments"); uint expiryTtl = request.GetQueryOrForm("expiryTtl", ZoneFile.ParseTtl, 0u); DnsResourceRecord oldRecord = null; DnsResourceRecord newRecord; switch (type) { case DnsResourceRecordType.A: case DnsResourceRecordType.AAAA: { IPAddress ipAddress = IPAddress.Parse(request.GetQueryOrFormAlt("ipAddress", "value")); IPAddress newIpAddress = IPAddress.Parse(request.GetQueryOrFormAlt("newIpAddress", "newValue", ipAddress.ToString())); bool ptr = request.GetQueryOrForm("ptr", bool.Parse, false); if (ptr) { string newPtrDomain = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 32 : 128); AuthZoneInfo newReverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(newPtrDomain); if (newReverseZoneInfo is null) { bool createPtrZone = request.GetQueryOrForm("createPtrZone", bool.Parse, false); if (!createPtrZone) throw new DnsWebServiceException("No reverse zone available to add PTR record."); string ptrZone = Zone.GetReverseZone(newIpAddress, type == DnsResourceRecordType.A ? 24 : 64); newReverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.CreatePrimaryZone(ptrZone); if (newReverseZoneInfo is null) throw new DnsWebServiceException("Failed to create reverse zone to add PTR record: " + ptrZone); //set permissions _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, sessionUser, PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SetPermission(PermissionSection.Zones, newReverseZoneInfo.Name, _dnsWebService._authManager.GetGroup(Group.DNS_ADMINISTRATORS), PermissionFlag.ViewModifyDelete); _dnsWebService._authManager.SaveConfigFile(); } if (newReverseZoneInfo.Internal) throw new DnsWebServiceException("Reverse zone '" + newReverseZoneInfo.DisplayName + "' is an internal zone."); if ((newReverseZoneInfo.Type != AuthZoneType.Primary) && (newReverseZoneInfo.Type != AuthZoneType.Forwarder)) throw new DnsWebServiceException("Reverse zone '" + newReverseZoneInfo.DisplayName + "' is not a primary or forwarder zone."); string oldPtrDomain = Zone.GetReverseZone(ipAddress, type == DnsResourceRecordType.A ? 32 : 128); AuthZoneInfo oldReverseZoneInfo = _dnsWebService._dnsServer.AuthZoneManager.FindAuthZoneInfo(oldPtrDomain); if ((oldReverseZoneInfo is not null) && !oldReverseZoneInfo.Internal && ((oldReverseZoneInfo.Type == AuthZoneType.Primary) || (oldReverseZoneInfo.Type == AuthZoneType.Forwarder))) { //delete old PTR record if any and save old reverse zone _dnsWebService._dnsServer.AuthZoneManager.DeleteRecords(oldReverseZoneInfo.Name, oldPtrDomain, DnsResourceRecordType.PTR); _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(oldReverseZoneInfo.Name); } //add new PTR record and save reverse zone DnsResourceRecord ptrRecord = new DnsResourceRecord(newPtrDomain, DnsResourceRecordType.PTR, DnsClass.IN, ttl, new DnsPTRRecordData(domain)); ptrRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow; ptrRecord.GetAuthGenericRecordInfo().ExpiryTtl = expiryTtl; _dnsWebService._dnsServer.AuthZoneManager.SetRecord(newReverseZoneInfo.Name, ptrRecord); _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(newReverseZoneInfo.Name); } if (type == DnsResourceRecordType.A) { oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsARecordData(ipAddress)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsARecordData(newIpAddress)); } else { oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsAAAARecordData(ipAddress)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsAAAARecordData(newIpAddress)); } } break; case DnsResourceRecordType.NS: { string nameServer = request.GetQueryOrFormAlt("nameServer", "value").Trim('.'); string newNameServer = request.GetQueryOrFormAlt("newNameServer", "newValue", nameServer).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsNSRecordData(nameServer)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsNSRecordData(newNameServer)); if (request.TryGetQueryOrForm("glue", out string glueAddresses)) { if (zoneInfo.Name.Equals(newDomain, StringComparison.OrdinalIgnoreCase) && (newNameServer.Equals(newDomain, StringComparison.OrdinalIgnoreCase) || newNameServer.EndsWith("." + newDomain, StringComparison.OrdinalIgnoreCase))) throw new DnsWebServiceException("The zone's own NS records cannot have glue addresses. Please add separate A/AAAA records in the zone instead."); newRecord.SetGlueRecords(glueAddresses); } if ((zoneInfo.Type == AuthZoneType.Primary) && zoneInfo.Name.Equals(domain, StringComparison.OrdinalIgnoreCase) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName)) { if (disable) throw new DnsWebServiceException("Cannot disable NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); if (expiryTtl > 0) throw new DnsWebServiceException("Cannot set automatic expiry TTL for NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); if (!nameServer.Equals(newNameServer, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Cannot update NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); if (!string.IsNullOrEmpty(glueAddresses)) throw new DnsWebServiceException("Cannot update NS records for Primary zones that are members of the Cluster Catalog zone. These NS records are automatically managed by the Cluster and only their TTL values can be updated."); } } break; case DnsResourceRecordType.CNAME: { string cname = request.GetQueryOrFormAlt("cname", "value").Trim('.'); if (cname.Equals(newDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("CNAME domain name cannot be same as that of the record name."); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCNAMERecordData(cname)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCNAMERecordData(cname)); } break; case DnsResourceRecordType.SOA: { string primaryNameServer = request.GetQueryOrForm("primaryNameServer").Trim('.'); string responsiblePerson = request.GetQueryOrForm("responsiblePerson").Trim('.'); uint serial = request.GetQueryOrForm("serial", uint.Parse); uint refresh = request.GetQueryOrForm("refresh", ZoneFile.ParseTtl); uint retry = request.GetQueryOrForm("retry", ZoneFile.ParseTtl); uint expire = request.GetQueryOrForm("expire", ZoneFile.ParseTtl); uint minimum = request.GetQueryOrForm("minimum", ZoneFile.ParseTtl); if ((zoneInfo.Type == AuthZoneType.Primary) && _dnsWebService._clusterManager.ClusterInitialized && _dnsWebService._clusterManager.IsClusterCatalogZone(zoneInfo.CatalogZoneName)) { if (!primaryNameServer.Equals(_dnsWebService._dnsServer.ServerDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("Cannot update SOA record for Primary zones that are members of the Cluster Catalog zone. The SOA primary name server field must match the Cluster Primary node's domain name."); } newRecord = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, new DnsSOARecordData(primaryNameServer, responsiblePerson, serial, refresh, retry, expire, minimum)); switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: { if (request.TryGetQueryOrForm("useSerialDateScheme", bool.Parse, out bool useSerialDateScheme)) newRecord.GetAuthSOARecordInfo().UseSoaSerialDateScheme = useSerialDateScheme; } break; } } break; case DnsResourceRecordType.PTR: { string ptrName = request.GetQueryOrFormAlt("ptrName", "value").Trim('.'); string newPtrName = request.GetQueryOrFormAlt("newPtrName", "newValue", ptrName).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsPTRRecordData(ptrName)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsPTRRecordData(newPtrName)); } break; case DnsResourceRecordType.MX: { ushort preference = request.GetQueryOrForm("preference", ushort.Parse); ushort newPreference = request.GetQueryOrForm("newPreference", ushort.Parse, preference); string exchange = request.GetQueryOrFormAlt("exchange", "value").Trim('.'); string newExchange = request.GetQueryOrFormAlt("newExchange", "newValue", exchange).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsMXRecordData(preference, exchange)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsMXRecordData(newPreference, newExchange)); } break; case DnsResourceRecordType.TXT: { string text = request.GetQueryOrFormAlt("text", "value"); string newText = request.GetQueryOrFormAlt("newText", "newValue", text); bool splitText = request.GetQueryOrForm("splitText", bool.Parse, false); bool newSplitText = request.GetQueryOrForm("newSplitText", bool.Parse, splitText); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, splitText ? new DnsTXTRecordData(DecodeCharacterStrings(text)) : new DnsTXTRecordData(text)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, newSplitText ? new DnsTXTRecordData(DecodeCharacterStrings(newText)) : new DnsTXTRecordData(newText)); } break; case DnsResourceRecordType.RP: { string mailbox = request.GetQueryOrForm("mailbox", "").Trim('.'); string newMailbox = request.GetQueryOrForm("newMailbox", mailbox).Trim('.'); string txtDomain = request.GetQueryOrForm("txtDomain", "").Trim('.'); string newTxtDomain = request.GetQueryOrForm("newTxtDomain", txtDomain).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsRPRecordData(mailbox, txtDomain)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsRPRecordData(newMailbox, newTxtDomain)); } break; case DnsResourceRecordType.SRV: { ushort priority = request.GetQueryOrForm("priority", ushort.Parse); ushort newPriority = request.GetQueryOrForm("newPriority", ushort.Parse, priority); ushort weight = request.GetQueryOrForm("weight", ushort.Parse); ushort newWeight = request.GetQueryOrForm("newWeight", ushort.Parse, weight); ushort port = request.GetQueryOrForm("port", ushort.Parse); ushort newPort = request.GetQueryOrForm("newPort", ushort.Parse, port); string target = request.GetQueryOrFormAlt("target", "value").Trim('.'); string newTarget = request.GetQueryOrFormAlt("newTarget", "newValue", target).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSRVRecordData(priority, weight, port, target)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSRVRecordData(newPriority, newWeight, newPort, newTarget)); } break; case DnsResourceRecordType.NAPTR: { ushort order = request.GetQueryOrForm("naptrOrder", ushort.Parse); ushort newOrder = request.GetQueryOrForm("naptrNewOrder", ushort.Parse, order); ushort preference = request.GetQueryOrForm("naptrPreference", ushort.Parse); ushort newPreference = request.GetQueryOrForm("naptrNewPreference", ushort.Parse, preference); string flags = request.GetQueryOrForm("naptrFlags", ""); string newFlags = request.GetQueryOrForm("naptrNewFlags", flags); string services = request.GetQueryOrForm("naptrServices", ""); string newServices = request.GetQueryOrForm("naptrNewServices", services); string regexp = request.GetQueryOrForm("naptrRegexp", ""); string newRegexp = request.GetQueryOrForm("naptrNewRegexp", regexp); string replacement = request.GetQueryOrForm("naptrReplacement", "").Trim('.'); string newReplacement = request.GetQueryOrForm("naptrNewReplacement", replacement).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsNAPTRRecordData(order, preference, flags, services, regexp, replacement)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsNAPTRRecordData(newOrder, newPreference, newFlags, newServices, newRegexp, newReplacement)); } break; case DnsResourceRecordType.DNAME: { string dname = request.GetQueryOrFormAlt("dname", "value").Trim('.'); if (dname.EndsWith("." + newDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("DNAME domain name cannot be a sub domain of the record name."); if (dname.Equals(newDomain, StringComparison.OrdinalIgnoreCase)) throw new DnsWebServiceException("DNAME domain name cannot be same as that of the record name."); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDNAMERecordData(dname)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDNAMERecordData(dname)); } break; case DnsResourceRecordType.DS: { ushort keyTag = request.GetQueryOrForm("keyTag", ushort.Parse); ushort newKeyTag = request.GetQueryOrForm("newKeyTag", ushort.Parse, keyTag); DnssecAlgorithm algorithm = Enum.Parse(request.GetQueryOrForm("algorithm").Replace('-', '_'), true); DnssecAlgorithm newAlgorithm = Enum.Parse(request.GetQueryOrForm("newAlgorithm", algorithm.ToString()).Replace('-', '_'), true); DnssecDigestType digestType = Enum.Parse(request.GetQueryOrForm("digestType").Replace('-', '_'), true); DnssecDigestType newDigestType = Enum.Parse(request.GetQueryOrForm("newDigestType", digestType.ToString()).Replace('-', '_'), true); byte[] digest = request.GetQueryOrFormAlt("digest", "value", Convert.FromHexString); byte[] newDigest = request.GetQueryOrFormAlt("newDigest", "newValue", Convert.FromHexString, digest); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsDSRecordData(keyTag, algorithm, digestType, digest)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsDSRecordData(newKeyTag, newAlgorithm, newDigestType, newDigest)); } break; case DnsResourceRecordType.SSHFP: { DnsSSHFPAlgorithm sshfpAlgorithm = request.GetQueryOrFormEnum("sshfpAlgorithm"); DnsSSHFPAlgorithm newSshfpAlgorithm = request.GetQueryOrFormEnum("newSshfpAlgorithm", sshfpAlgorithm); DnsSSHFPFingerprintType sshfpFingerprintType = request.GetQueryOrFormEnum("sshfpFingerprintType"); DnsSSHFPFingerprintType newSshfpFingerprintType = request.GetQueryOrFormEnum("newSshfpFingerprintType", sshfpFingerprintType); byte[] sshfpFingerprint = request.GetQueryOrForm("sshfpFingerprint", Convert.FromHexString); byte[] newSshfpFingerprint = request.GetQueryOrForm("newSshfpFingerprint", Convert.FromHexString, sshfpFingerprint); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSSHFPRecordData(sshfpAlgorithm, sshfpFingerprintType, sshfpFingerprint)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSSHFPRecordData(newSshfpAlgorithm, newSshfpFingerprintType, newSshfpFingerprint)); } break; case DnsResourceRecordType.TLSA: { DnsTLSACertificateUsage tlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("tlsaCertificateUsage").Replace('-', '_'), true); DnsTLSACertificateUsage newTlsaCertificateUsage = Enum.Parse(request.GetQueryOrForm("newTlsaCertificateUsage", tlsaCertificateUsage.ToString()).Replace('-', '_'), true); DnsTLSASelector tlsaSelector = request.GetQueryOrFormEnum("tlsaSelector"); DnsTLSASelector newTlsaSelector = request.GetQueryOrFormEnum("newTlsaSelector", tlsaSelector); DnsTLSAMatchingType tlsaMatchingType = Enum.Parse(request.GetQueryOrForm("tlsaMatchingType").Replace('-', '_'), true); DnsTLSAMatchingType newTlsaMatchingType = Enum.Parse(request.GetQueryOrForm("newTlsaMatchingType", tlsaMatchingType.ToString()).Replace('-', '_'), true); string tlsaCertificateAssociationData = request.GetQueryOrForm("tlsaCertificateAssociationData"); string newTlsaCertificateAssociationData = request.GetQueryOrForm("newTlsaCertificateAssociationData", tlsaCertificateAssociationData); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsTLSARecordData(tlsaCertificateUsage, tlsaSelector, tlsaMatchingType, tlsaCertificateAssociationData)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsTLSARecordData(newTlsaCertificateUsage, newTlsaSelector, newTlsaMatchingType, newTlsaCertificateAssociationData)); } break; case DnsResourceRecordType.SVCB: case DnsResourceRecordType.HTTPS: { ushort svcPriority = request.GetQueryOrForm("svcPriority", ushort.Parse); ushort newSvcPriority = request.GetQueryOrForm("newSvcPriority", ushort.Parse, svcPriority); string targetName = request.GetQueryOrForm("svcTargetName").Trim('.'); string newTargetName = request.GetQueryOrForm("newSvcTargetName", targetName).Trim('.'); string strSvcParams = request.GetQueryOrForm("svcParams"); string strNewSvcParams = request.GetQueryOrForm("newSvcParams", strSvcParams); bool autoIpv4Hint = request.GetQueryOrForm("autoIpv4Hint", bool.Parse, false); bool autoIpv6Hint = request.GetQueryOrForm("autoIpv6Hint", bool.Parse, false); Dictionary svcParams; if (strSvcParams.Equals("false", StringComparison.OrdinalIgnoreCase)) { svcParams = new Dictionary(0); } else { string[] strSvcParamsParts = strSvcParams.Split('|'); svcParams = new Dictionary(strSvcParamsParts.Length / 2); for (int i = 0; i < strSvcParamsParts.Length; i += 2) { DnsSvcParamKey svcParamKey = Enum.Parse(strSvcParamsParts[i].Replace('-', '_'), true); DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]); svcParams.Add(svcParamKey, svcParamValue); } } Dictionary newSvcParams; if (strNewSvcParams.Equals("false", StringComparison.OrdinalIgnoreCase)) { newSvcParams = new Dictionary(0); } else { string[] strSvcParamsParts = strNewSvcParams.Split('|'); newSvcParams = new Dictionary(strSvcParamsParts.Length / 2); for (int i = 0; i < strSvcParamsParts.Length; i += 2) { DnsSvcParamKey svcParamKey = Enum.Parse(strSvcParamsParts[i].Replace('-', '_'), true); DnsSvcParamValue svcParamValue = DnsSvcParamValue.Parse(svcParamKey, strSvcParamsParts[i + 1]); newSvcParams.Add(svcParamKey, svcParamValue); } } oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsSVCBRecordData(svcPriority, targetName, svcParams)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsSVCBRecordData(newSvcPriority, newTargetName, newSvcParams)); if (autoIpv4Hint) newRecord.GetAuthSVCBRecordInfo().AutoIpv4Hint = true; if (autoIpv6Hint) newRecord.GetAuthSVCBRecordInfo().AutoIpv6Hint = true; if (autoIpv4Hint || autoIpv6Hint) ResolveSvcbAutoHints(zoneInfo.Name, newRecord, autoIpv4Hint, autoIpv6Hint, newSvcParams); } break; case DnsResourceRecordType.URI: { ushort priority = request.GetQueryOrForm("uriPriority", ushort.Parse); ushort newPriority = request.GetQueryOrForm("newUriPriority", ushort.Parse, priority); ushort weight = request.GetQueryOrForm("uriWeight", ushort.Parse); ushort newWeight = request.GetQueryOrForm("newUriWeight", ushort.Parse, weight); Uri uri = request.GetQueryOrForm("uri", delegate (string value) { return new Uri(value); }); Uri newUri = request.GetQueryOrForm("newUri", delegate (string value) { return new Uri(value); }, uri); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsURIRecordData(priority, weight, uri)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsURIRecordData(newPriority, newWeight, newUri)); } break; case DnsResourceRecordType.CAA: { byte flags = request.GetQueryOrForm("flags", byte.Parse); byte newFlags = request.GetQueryOrForm("newFlags", byte.Parse, flags); string tag = request.GetQueryOrForm("tag"); string newTag = request.GetQueryOrForm("newTag", tag); string value = request.GetQueryOrForm("value"); string newValue = request.GetQueryOrForm("newValue", value); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsCAARecordData(flags, tag, value)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsCAARecordData(newFlags, newTag, newValue)); } break; case DnsResourceRecordType.ANAME: { string aname = request.GetQueryOrFormAlt("aname", "value").Trim('.'); string newAName = request.GetQueryOrFormAlt("newAName", "newValue", aname).Trim('.'); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsANAMERecordData(aname)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsANAMERecordData(newAName)); } break; case DnsResourceRecordType.FWD: { DnsTransportProtocol protocol = request.GetQueryOrFormEnum("protocol", DnsTransportProtocol.Udp); DnsTransportProtocol newProtocol = request.GetQueryOrFormEnum("newProtocol", protocol); string forwarder = request.GetQueryOrFormAlt("forwarder", "value"); string newForwarder = request.GetQueryOrFormAlt("newForwarder", "newValue", forwarder); bool dnssecValidation = request.GetQueryOrForm("dnssecValidation", bool.Parse, false); DnsForwarderRecordProxyType proxyType = DnsForwarderRecordProxyType.DefaultProxy; string proxyAddress = null; ushort proxyPort = 0; string proxyUsername = null; string proxyPassword = null; if (!newForwarder.Equals("this-server")) { proxyType = request.GetQueryOrFormEnum("proxyType", DnsForwarderRecordProxyType.DefaultProxy); switch (proxyType) { case DnsForwarderRecordProxyType.Http: case DnsForwarderRecordProxyType.Socks5: proxyAddress = request.GetQueryOrForm("proxyAddress"); proxyPort = request.GetQueryOrForm("proxyPort", ushort.Parse); proxyUsername = request.QueryOrForm("proxyUsername"); proxyPassword = request.QueryOrForm("proxyPassword"); break; } } byte priority = request.GetQueryOrForm("forwarderPriority", byte.Parse, byte.MinValue); if (newProtocol == DnsTransportProtocol.Quic) DnsWebService.ValidateQuicSupport(); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, DnsForwarderRecordData.CreatePartialRecordData(protocol, forwarder)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, 0, new DnsForwarderRecordData(newProtocol, newForwarder, dnssecValidation, proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword, priority)); } break; case DnsResourceRecordType.APP: { string appName = request.GetQueryOrFormAlt("appName", "value"); string classPath = request.GetQueryOrForm("classPath"); string recordData = request.GetQueryOrForm("recordData", ""); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsApplicationRecordData(appName, classPath, recordData)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsApplicationRecordData(appName, classPath, recordData)); } break; default: { string strRData = request.GetQueryOrForm("rdata"); string strNewRData = request.GetQueryOrForm("newRData", strRData); byte[] rdata; if (strRData.Contains(':')) rdata = strRData.ParseColonHexString(); else rdata = Convert.FromHexString(strRData); byte[] newRData; if (strNewRData.Contains(':')) newRData = strNewRData.ParseColonHexString(); else newRData = Convert.FromHexString(strNewRData); oldRecord = new DnsResourceRecord(domain, type, DnsClass.IN, 0, new DnsUnknownRecordData(rdata)); newRecord = new DnsResourceRecord(newDomain, type, DnsClass.IN, ttl, new DnsUnknownRecordData(newRData)); } break; } //update record info GenericRecordInfo recordInfo = newRecord.GetAuthGenericRecordInfo(); recordInfo.LastModified = DateTime.UtcNow; recordInfo.ExpiryTtl = expiryTtl; recordInfo.Disabled = disable; recordInfo.Comments = comments; //update record if (type == DnsResourceRecordType.SOA) { //special SOA case switch (zoneInfo.Type) { case AuthZoneType.Primary: case AuthZoneType.Forwarder: case AuthZoneType.Catalog: _dnsWebService._dnsServer.AuthZoneManager.SetRecord(zoneInfo.Name, newRecord); break; } //get updated record to return json newRecord = zoneInfo.ApexZone.GetRecords(DnsResourceRecordType.SOA)[0]; } else { _dnsWebService._dnsServer.AuthZoneManager.UpdateRecord(zoneInfo.Name, oldRecord, newRecord); } //additional processing if ((type == DnsResourceRecordType.A) || (type == DnsResourceRecordType.AAAA)) { bool updateSvcbHints = request.GetQueryOrForm("updateSvcbHints", bool.Parse, false); if (updateSvcbHints) UpdateSvcbAutoHints(zoneInfo.Name, newDomain, type == DnsResourceRecordType.A, type == DnsResourceRecordType.AAAA); } _dnsWebService._log.Write(context.GetRemoteEndPoint(_dnsWebService._webServiceRealIpHeader), "[" + sessionUser.Username + "] Record was updated for " + zoneInfo.TypeName + " zone '" + zoneInfo.DisplayName + "' successfully {" + (oldRecord is null ? "" : "oldRecord: " + oldRecord.ToString() + "; ") + "newRecord: " + newRecord.ToString() + "}"); //save zone _dnsWebService._dnsServer.AuthZoneManager.SaveZoneFile(zoneInfo.Name); Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter(); jsonWriter.WritePropertyName("zone"); WriteZoneInfoAsJson(zoneInfo, jsonWriter); jsonWriter.WritePropertyName("updatedRecord"); WriteRecordAsJson(newRecord, jsonWriter, true, zoneInfo); } #endregion } } } ================================================ FILE: DnsServerCore/dohwww/css/main.css ================================================  html, body { height: 100% !important; } body { margin: 0px !important; line-height: 1.42857143 !important; } a { color: #6699ff; } a:hover { color: #6699ff; } #content { min-height: 100%; } .container { margin-left: auto; margin-right: auto; padding: 55px 15px 60px 15px; word-wrap: break-word; } .container .pageLogin { display: none; margin: auto; width: 500px; padding: 150px 0 0 0; } .container .page { display: none; } .features { margin: 80px auto 40px auto; font-family: Arial; } .features .pull-left { width: 50%; } .features .pull-right { width: 50%; } .features h3 { font-size: 22px; text-align: center; } .features p { color: rgb(119, 119, 119); text-align: center; } .features li { color: rgb(119, 119, 119); } .shadow-screenshot { box-shadow: 2px 3px 15px 1px #888888; } .auto-resize-img { max-width: 100%; height: auto; display: block; margin-right: auto; margin-left: auto; } @media (min-width: 992px) { #header .title, .container, #footer .content { width: 970px; } } @media (min-width: 1200px) { #header .title, .container, #footer .content { width: 1170px; } .stats-panel .stats-item { padding: 6px !important; } } ================================================ FILE: DnsServerCore/dohwww/index.html ================================================ Technitium DNS Server
Technitium Logo

Technitium DNS Server

This server supports encrypted DNS protocol (DNS-over-HTTPS) that you can use with your web browser like Mozilla Firefox.

The Encrypted DNS Service URL

Use the following URL to configure your clients for consuming the DNS-over-HTTPS service.

Mozilla Firefox Configuration

To configure Firefox, go to Settings > Privacy & Security and scroll down to find DNS over HTTPS section. Click on the Max Protection option, select Custom option in the Choose provider drop down box, and enter the encrypted DNS service URL given above.

Mozilla Firefox Custom DNS-over-HTTPS Option

Note! This will work only for Firefox and all other applications on your computer will keep using the default DNS server configured in your network settings.

================================================ FILE: DnsServerCore/dohwww/js/main.js ================================================ /* Technitium DNS Server Copyright (C) 2021 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 . */ $(function () { var link = "https://" + window.location.hostname + "/dns-query"; var lnkDoH = $("#lnkDoH"); lnkDoH.text(link); lnkDoH.attr("href", link); }); ================================================ FILE: DnsServerCore/dohwww/robots.txt ================================================ User-agent: * Disallow: / ================================================ FILE: DnsServerCore/named.root ================================================ ; This file holds the information on root name servers needed to ; initialize cache of Internet domain name servers ; (e.g. reference this file in the "cache . " ; configuration file of BIND domain name servers). ; ; This file is made available by InterNIC ; under anonymous FTP as ; file /domain/named.cache ; on server FTP.INTERNIC.NET ; -OR- RS.INTERNIC.NET ; ; last update: October 29, 2025 ; related version of root zone: 2025102901 ; ; FORMERLY NS.INTERNIC.NET ; . 3600000 NS A.ROOT-SERVERS.NET. A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4 A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30 ; ; FORMERLY NS1.ISI.EDU ; . 3600000 NS B.ROOT-SERVERS.NET. B.ROOT-SERVERS.NET. 3600000 A 170.247.170.2 B.ROOT-SERVERS.NET. 3600000 AAAA 2801:1b8:10::b ; ; FORMERLY C.PSI.NET ; . 3600000 NS C.ROOT-SERVERS.NET. C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12 C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c ; ; FORMERLY TERP.UMD.EDU ; . 3600000 NS D.ROOT-SERVERS.NET. D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13 D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d ; ; FORMERLY NS.NASA.GOV ; . 3600000 NS E.ROOT-SERVERS.NET. E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10 E.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:a8::e ; ; FORMERLY NS.ISC.ORG ; . 3600000 NS F.ROOT-SERVERS.NET. F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241 F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f ; ; FORMERLY NS.NIC.DDN.MIL ; . 3600000 NS G.ROOT-SERVERS.NET. G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4 G.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:12::d0d ; ; FORMERLY AOS.ARL.ARMY.MIL ; . 3600000 NS H.ROOT-SERVERS.NET. H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53 H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53 ; ; FORMERLY NIC.NORDU.NET ; . 3600000 NS I.ROOT-SERVERS.NET. I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17 I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53 ; ; OPERATED BY VERISIGN, INC. ; . 3600000 NS J.ROOT-SERVERS.NET. J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30 J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30 ; ; OPERATED BY RIPE NCC ; . 3600000 NS K.ROOT-SERVERS.NET. K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129 K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1 ; ; OPERATED BY ICANN ; . 3600000 NS L.ROOT-SERVERS.NET. L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42 L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42 ; ; OPERATED BY WIDE ; . 3600000 NS M.ROOT-SERVERS.NET. M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33 M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35 ; End of file ================================================ FILE: DnsServerCore/root-anchors.xml ================================================ . 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU= 257 38696 8 2 683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16 AwEAAa96jeuknZlaeSrvyAJj6ZHv28hhOKkx3rLGXVaC6rXTsDc449/cidltpkyGwCJNnOAlFNKF2jBosZBU5eeHspaQWOmOElZsjICMQMC3aeHbGiShvZsx4wMYSjH8e7Vrhbu6irwCzVBApESjbUdpWWmEnhathWu1jo+siFUiRAAxm9qyJNg/wOZqqzL/dL/q8PkcRU5oUKEpUge71M3ej2/7CPqpdVwuMoTvoB+ZOT4YeGyxMvHmbrxlFzGOHOijtzN+u1TQNatX2XBuzZNQ1K+s2CXkPIZo7s6JgZyvaBevYtxPvYLw4z9mR7K2vaF18UYH9Z9GNUUeayffKC73PYc= 257 ================================================ FILE: DnsServerCore/www/css/main.css ================================================ html, body { height: 100% !important; scrollbar-gutter: stable; } body.modal-open { padding-right: 0 !important; } body { margin: 0px !important; line-height: 1.42857143 !important; } a { color: #6699ff; } a:hover { color: #6699ff; } a:visited { color: #6699ff; } th a { color: black !important; } th a:hover { color: black; text-decoration: none; } #header { background-color: #6699ff; height: 32px; margin-bottom: -32px; box-shadow: 0px 1px 15px 0px #888888; width: 100%; min-width: 970px; } #header .title { margin: 0 auto; color: #ffffff; padding: 0px 15px 0px 15px; } #header .title img { vertical-align: text-bottom; } #header .title .text { font-size: 24px; font-weight: 600; font-family: Arial; margin-left: 4px; } #header .menu { float: right; padding: 6px; } #header .menu .menu-title { color: #ffffff; font-family: Arial; font-size: 16px; } #content { min-height: 100%; } .container { margin-left: auto; margin-right: auto; padding: 55px 15px 60px 15px; word-wrap: break-word; min-width: 970px; } .container .pageLogin { display: none; margin: auto; width: 500px; padding: 150px 0 0 0; } .container .page { display: none; } .auto-resize-img { max-width: 100%; height: auto; display: block; margin-right: auto; margin-left: auto; } .center-iframe { display: block; margin-right: auto; margin-left: auto; max-width: 640px; max-height: 480px; } .center-iframe iframe { width: 100%; height: 100%; } #footer { background-color: rgb(243, 243, 243); padding: 20px 0px 20px 0px; margin-top: -55px; box-shadow: 0px 2px 15px 1px #888888; clear: both; position: relative; height: 55px; min-width: 970px; } #footer .content { margin: 0 auto; color: rgb(119,119,119); font-family: Arial, sans-serif; font-size: 11px; font-weight: 600; text-align: center; } #footer .content a { color: #6699ff; text-decoration: none; } #footer .content a:hover { color: #6699ff; } @media (min-width: 992px) { #header .title, .container, #footer .content { width: 970px; } } @media (min-width: 1200px) { #header .title, .container, #footer .content { width: 1170px; } } .form-inline .form-group { margin-right: 10px; margin-bottom: 10px; } .AlertPlaceholder { position: fixed; width: 800px; margin: auto; left: 0; right: 0; top: 45px; z-index: 1000; } .zone-list-pane { float: left; width: 24%; } .zones { font-size: 14px; } .zones .zone { padding: 4px; } .zone-viewer-pane { float: right; width: 75%; display: none; } .log-list-pane { float: left; width: 17%; } .logs { font-size: 12px; } .logs .log { padding: 2px; } .log-viewer-pane { float: right; width: 82%; display: none; } .query-logs tr:hover { backdrop-filter: brightness(95%); } .stats-panel { height: 80px; padding: 6px 0 6px 0; } .stats-panel .total-queries { background-color: rgba(102, 153, 255, 0.7); color: #ffffff; } .stats-panel .no-error { background-color: rgba(92, 184, 92, 0.7); color: #ffffff; } .stats-panel .server-failure { background-color: rgba(217, 83, 79, 0.7); color: #ffffff; } .stats-panel .nxdomain { background-color: rgba(120, 120, 120, 0.7); color: #ffffff; } .stats-panel .refused { background-color: rgba(91, 192, 222, 0.7); color: #ffffff; } .stats-panel .auth-hit { background-color: rgba(150, 150, 0, 0.7); color: #ffffff; } .stats-panel .cache-hit { background-color: rgba(111, 84, 153, 0.7); color: #ffffff; } .stats-panel .blocked { background-color: rgba(255, 165, 0, 0.7); color: #ffffff; } .stats-panel .dropped { background-color: rgba(30, 30, 30, 0.7); color: #ffffff; } .stats-panel .recursions { background-color: rgba(23, 162, 184, 0.7); color: #ffffff; } .stats-panel .clients { background-color: rgba(51, 122, 183, 0.7); color: #ffffff; } .stats-panel .stats-last-item { margin-right: 0% !important; } .stats-panel .stats-item { width: 8.818%; float: left; padding: 4px; margin-right: 0.3%; } .stats-panel .stats-item .number { font-size: 15px; font-weight: bold; } .stats-panel .stats-item .percentage { font-size: 10px; font-weight: bold; } .stats-panel .stats-item .title { font-size: 12px; font-weight: bold; } .zone-stats-panel { margin-bottom: 15px; } .zone-stats-panel .stats-last-item { margin-right: 0% !important; } .zone-stats-panel .stats-item { width: 16.125%; float: left; padding: 6px 4px; margin-right: 0.65%; background-color: rgba(51, 122, 183, 0.7); color: #ffffff; } .zone-stats-panel .stats-item .number { font-size: 14px; font-weight: bold; padding: 6px 0; } .zone-stats-panel .stats-item .title { font-size: 12px; font-weight: bold; } .about p { color: rgb(119, 119, 119); text-align: center; } .about h3 a { color: rgb(51,51,51) !important; } .cluster-node-dropdown { margin-left: 4px; padding: 2px 8px; height: 28px; max-width: 250px; } .dark-mode { scrollbar-width: thin; scrollbar-color: #555 #2c2c2e; } .dark-mode ::-webkit-scrollbar { width: 12px; height: 12px; } .dark-mode ::-webkit-scrollbar-track { background: #2c2c2e; } .dark-mode ::-webkit-scrollbar-thumb { background-color: #555; border-radius: 6px; border: 3px solid #2c2c2e; } body.dark-mode { background-color: #1a1a1a !important; color: #dcdcdc !important; } .dark-mode th a, .dark-mode th a:hover { color: #dcdcdc !important; text-decoration: none !important; } .dark-mode #header { background-color: #2c2c2e !important; box-shadow: 0px 1px 15px 0px #000 !important; } .dark-mode #footer { background-color: #252525 !important; color: #888888 !important; box-shadow: 0px 2px 15px 1px #000 !important; } .dark-mode #footer .content { color: #888888 !important; } .dark-mode .about h1 { color: #f0f0f0 !important; } .dark-mode .about p { color: #a0a0a0 !important; } .dark-mode .about h3 a { color: #f0f0f0 !important; } .dark-mode .panel, .dark-mode .panel-default { background-color: #2c2c2e !important; border-color: #3a3a3c !important; } .dark-mode .panel-heading { background-color: #3a3a3c !important; border-color: #4a4a4c !important; color: #f5f5f7 !important; } .dark-mode .panel-body { background-color: #252525 !important; color: #dcdcdc !important; } .dark-mode .navbar-default { background-color: #2c2c2e !important; border-color: #3a3a3c !important; } .dark-mode .dropdown-menu { background-color: #2c2c2e !important; border-color: #3a3a3c !important; } .dark-mode .dropdown-menu > li > a:hover, .dark-mode .dropdown-menu > li > a:focus { background-color: #3a3a3c !important; } .dark-mode .dropdown-menu li a { color: #ffffff !important; } .dark-mode .divider { background-color: #3a3a3c !important; } .dark-mode .nav-tabs { border-bottom: 1px solid #007aff !important; } .dark-mode .nav-tabs > li > a:hover { border-color: #4a4a4c !important; border-bottom-color: #007aff !important; background-color: #3a3a3c; } .dark-mode .nav-tabs > li.active > a, .dark-mode .nav-tabs > li.active > a:hover, .dark-mode .nav-tabs > li.active > a:focus { background-color: #252525; border-color: #007aff !important; color: #ffffff !important; border-bottom-color: transparent !important; } .dark-mode .table-hover > tbody > tr:hover { background-color: #33373a !important; } .dark-mode .table-striped > tbody > tr:nth-of-type(odd) { background-color: #28282a !important; } .dark-mode .table > thead > tr > th, .dark-mode .table > tbody > tr > td, .dark-mode .table > tfoot > tr > th, .dark-mode .table > tfoot > tr > td { border-top-color: #3a3a3c !important; } .dark-mode .table-bordered, .dark-mode .table-bordered > thead > tr > th, .dark-mode .table-bordered > tbody > tr > td { border-color: #3a3a3c !important; } .dark-mode .form-control { background-color: #3a3a3c !important; border-color: #4a4a4c !important; color: #f5f5f7 !important; } .dark-mode .form-control[disabled], .dark-mode .form-control[readonly] { background-color: #2c2c2e !important; } .dark-mode .modal-content { background-color: #2c2c2e !important; border-color: #3a3a3c !important; } .dark-mode .modal-header { border-bottom-color: #3a3a3c !important; } .dark-mode .modal-footer { border-top-color: #3a3a3c !important; } .dark-mode .well { background-color: #252525 !important; border-color: #3a3a3c !important; } .dark-mode pre { background-color: #222 !important; color: #dcdcdc !important; border: 1px solid #4a4a4c !important; } .dark-mode .btn-default { color: #f5f5f7 !important; background-color: #3a3a3c !important; border-color: #4a4a4c !important; } .dark-mode .btn-default:hover, .dark-mode .btn-default.active { background-color: #4a4a4c !important; border-color: #5a5a5c !important; } .dark-mode .input-group-addon { background-color: #3a3a3c !important; border-color: #4a4a4c !important; } .dark-mode .stats-panel .stats-item, .dark-mode .zone-stats-panel .stats-item { color: #fff !important; } .dark-mode .c3-axis-x text, .dark-mode .c3-axis-y text, .dark-mode .c3-legend-item { fill: #dcdcdc !important; } .dark-mode .c3-grid line { stroke: #4a4a4c !important; } .dark-mode input[type="datetime-local"] { color-scheme: dark; } .dark-mode input[type="datetime-local"]::-webkit-calendar-picker-indicator { filter: invert(1); } .dark-mode .pager li > a { background-color: #3a3a3c !important; border-color: #4a4a4c !important; } .dark-mode .pager li > a:hover { background-color: #4a4a4c !important; } .dark-mode .pagination li:not(.active) a { color: white; background-color: #252525; border: 1px solid #337ab7; } .dark-mode .pagination li:not(.active) a:hover { background-color: #33373a; } .dark-mode #dpCustomDayWiseStart, .dark-mode #dpCustomDayWiseEnd, .dark-mode #txtQueryLogStart, .dark-mode #txtQueryLogEnd { color-scheme: dark; } .dark-mode #dpCustomDayWiseStart::-webkit-calendar-picker-indicator, .dark-mode #dpCustomDayWiseEnd::-webkit-calendar-picker-indicator, .dark-mode #txtQueryLogStart::-webkit-calendar-picker-indicator, .dark-mode #txtQueryLogEnd::-webkit-calendar-picker-indicator { filter: invert(1); } ================================================ FILE: DnsServerCore/www/index.html ================================================  Technitium DNS Server

DNS Server

Enter the 6-digit code you see in your authenticator app.
0 zones
# Zone Type DNSSEC Status Serial Expiry Last Modified
0 zones
technitium.com

                                                
technitium.com

                                                
technitium.com

                                                
Installed Apps
Total Apps: 0
20171012
Technitium Logo

Technitium DNS Server

Version

Server up since

Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions.

Source code available under GNU General Public License v3.0 on  GitHub

What's New?

Read the change log to know what's new in this release.

API Documentation

The DNS server HTTP API allows any 3rd party app or script to configure the DNS server. The HTTP API is used by this web console and thus all the actions that this web console does can be performed via the API. Read the HTTP API documentation for complete details.

Help Topics

Read the latest online help topics which contains the DNS Server user manual and covers frequently asked questions.

Support

For support, send an email to support@technitium.com.

Follow @technitium@mastodon.social on Mastodon.
Checkout Technitium Blog.

Join /r/technitium on Reddit.

Donate

Make a contribution to Technitium and help making new software, updates, and features possible.

Donate Now!

================================================ FILE: DnsServerCore/www/js/apps.js ================================================ /* 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 . */ function refreshApps() { var divViewAppsLoader = $("#divViewAppsLoader"); var divViewApps = $("#divViewApps"); divViewApps.hide(); divViewAppsLoader.show(); HTTPRequest({ url: "api/apps/list?token=" + sessionData.token, success: function (responseJSON) { var apps = responseJSON.response.apps; var tableHtmlRows = ""; for (var i = 0; i < apps.length; i++) { tableHtmlRows += getAppRowHtml(apps[i]); } $("#tableAppsBody").html(tableHtmlRows); if (apps.length > 0) $("#tableAppsFooter").html("Total Apps: " + apps.length + ""); else $("#tableAppsFooter").html("No Apps Found"); divViewAppsLoader.hide(); divViewApps.show(); }, error: function () { divViewAppsLoader.hide(); divViewApps.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divViewAppsLoader }); } function getAppRowId(appName) { return btoa(appName).replace(/=/g, ""); } function getAppRowHtml(app) { var name = app.name; var version = app.version; var updateVersion = app.updateVersion; var updateUrl = app.updateUrl; var updateAvailable = app.updateAvailable; var dnsAppsTable = null; //dnsApps if (app.dnsApps.length > 0) { dnsAppsTable = ""; for (var j = 0; j < app.dnsApps.length; j++) { var labels = ""; var description = null; if (app.dnsApps[j].isAppRecordRequestHandler) { labels += "APP Record"; description = "

" + htmlEncode(app.dnsApps[j].description).replace(/\n/g, "
") + "

" + (app.dnsApps[j].recordDataTemplate == null ? "" : "
Record Data Template
" + htmlEncode(app.dnsApps[j].recordDataTemplate) + "
"); } if (app.dnsApps[j].isRequestController) labels += "Access Control"; if (app.dnsApps[j].isAuthoritativeRequestHandler) labels += "Authoritative"; if (app.dnsApps[j].isRequestBlockingHandler) labels += "Blocking"; if (app.dnsApps[j].isQueryLogger) labels += "Query Logger"; if (app.dnsApps[j].isQueryLogs) labels += "Query Logs"; if (app.dnsApps[j].isPostProcessor) labels += "Post Processor"; if (labels == "") labels = "Generic"; if (description == null) description = htmlEncode(app.dnsApps[j].description).replace(/\n/g, "
"); dnsAppsTable += ""; } dnsAppsTable += "
Class PathDescription
" + htmlEncode(app.dnsApps[j].classPath) + "
" + labels + "
" + description + "
" } var id = getAppRowId(name); var tableHtmlRow = "
" + htmlEncode(name) + "
Version " + htmlEncode(version) + " Update " + htmlEncode(updateVersion) + "
"; if (app.description != null) tableHtmlRow += "
" + htmlEncode(app.description).replace(/\n/g, "
") + "
"; if (dnsAppsTable != null) { tableHtmlRow += "
More Details "; tableHtmlRow += "
"; tableHtmlRow += dnsAppsTable; tableHtmlRow += "
"; } tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; return tableHtmlRow } function showStoreAppsModal() { var divStoreAppsAlert = $("#divStoreAppsAlert"); var divStoreAppsLoader = $("#divStoreAppsLoader"); var divStoreApps = $("#divStoreApps"); divStoreAppsLoader.show(); divStoreApps.hide(); $("#modalStoreApps").modal("show"); HTTPRequest({ url: "api/apps/listStoreApps?token=" + sessionData.token, success: function (responseJSON) { var storeApps = responseJSON.response.storeApps; var tableHtmlRows = ""; for (var i = 0; i < storeApps.length; i++) { var id = Math.floor(Math.random() * 10000); var name = storeApps[i].name; var version = storeApps[i].version; var description = storeApps[i].description; var url = storeApps[i].url; var size = storeApps[i].size; var installed = storeApps[i].installed; var installedVersion = storeApps[i].installedVersion; var updateAvailable = installed ? storeApps[i].updateAvailable : false; var displayVersion = installed ? installedVersion : version; description = htmlEncode(description).replace(/\n/g, "
"); tableHtmlRows += "
" + htmlEncode(name) + "
Version " + htmlEncode(displayVersion) + " Update " + htmlEncode(version) + "
"; tableHtmlRows += "
" + description + "
App Zip File: " + htmlEncode(url) + "
Size: " + htmlEncode(size) + "
"; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; } $("#tableStoreAppsBody").html(tableHtmlRows); if (storeApps.length > 0) $("#tableStoreAppsFooter").html("Total Apps: " + storeApps.length + ""); else $("#tableStoreAppsFooter").html("No Apps Found"); divStoreAppsLoader.hide(); divStoreApps.show(); }, error: function () { divStoreAppsLoader.hide(); divStoreApps.show(); }, invalidToken: function () { $("#modalStoreApps").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divStoreAppsAlert, objLoaderPlaceholder: divStoreAppsLoader }); } function showInstallAppModal() { $("#divInstallAppAlert").html(""); $("#txtInstallApp").val(""); $("#fileAppZip").val(""); $("#btnInstallApp").button("reset"); $("#modalInstallApp").modal("show"); setTimeout(function () { $("#txtInstallApp").trigger("focus"); }, 1000); } function showUpdateAppModal(appName) { $("#divUpdateAppAlert").html(""); $("#txtUpdateApp").val(appName); $("#fileUpdateAppZip").val(""); $("#btnUpdateApp").button("reset"); $("#modalUpdateApp").modal("show"); } function installStoreApp(objBtn, appName, url) { var divStoreAppsAlert = $("#divStoreAppsAlert"); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/apps/downloadAndInstall?token=" + sessionData.token + "&name=" + encodeURIComponent(appName) + "&url=" + encodeURIComponent(url), success: function (responseJSON) { btn.button("reset"); btn.hide(); var id = btn.attr("data-id"); $("#btnStoreAppUninstall" + id).show(); var tableHtmlRow = getAppRowHtml(responseJSON.response.installedApp); $("#tableAppsBody").prepend(tableHtmlRow); updateAppsFooterCount(); showAlert("success", "Store App Installed!", "DNS application '" + appName + "' was installed successfully from DNS App Store.", divStoreAppsAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalStoreApps").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divStoreAppsAlert }); } function updateStoreApp(objBtn, appName, url, isModal) { var divStoreAppsAlert; if (isModal) divStoreAppsAlert = $("#divStoreAppsAlert"); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/apps/downloadAndUpdate?token=" + sessionData.token + "&name=" + encodeURIComponent(appName) + "&url=" + encodeURIComponent(url), success: function (responseJSON) { btn.button("reset"); btn.hide(); if (isModal) { var id = btn.attr("data-id"); $("#spanStoreAppUpdateVersion" + id).hide(); $("#spanStoreAppDisplayVersion" + id).text($("#spanStoreAppUpdateVersion" + id).text().replace(/Update/g, "Version")); } var tableHtmlRow = getAppRowHtml(responseJSON.response.updatedApp); var id = getAppRowId(responseJSON.response.updatedApp.name); $("#trApp" + id).replaceWith(tableHtmlRow); showAlert("success", "Store App Updated!", "DNS application '" + appName + "' was updated successfully from DNS App Store.", divStoreAppsAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalStoreApps").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divStoreAppsAlert }); } function uninstallStoreApp(objBtn, appName) { if (!confirm("Are you sure you want to uninstall the DNS application '" + appName + "'?")) return; var divStoreAppsAlert = $("#divStoreAppsAlert"); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/apps/uninstall?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), success: function (responseJSON) { btn.button("reset"); btn.hide(); var id = btn.attr("data-id"); $("#btnStoreAppInstall" + id).show(); $("#btnStoreAppUpdate" + id).hide(); $("#spanStoreAppVersion" + id).attr("class", "label label-primary"); var id = getAppRowId(appName); $("#trApp" + id).remove(); updateAppsFooterCount(); showAlert("success", "Store App Uninstalled!", "DNS application '" + appName + "' was uninstalled successfully.", divStoreAppsAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalStoreApps").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divStoreAppsAlert }); } function installApp() { var divInstallAppAlert = $("#divInstallAppAlert"); var appName = $("#txtInstallApp").val(); if ((appName === null) || (appName === "")) { showAlert("warning", "Missing!", "Please enter an application name.", divInstallAppAlert); $("#txtInstallApp").trigger("focus"); return; } var fileAppZip = $("#fileAppZip"); if (fileAppZip[0].files.length === 0) { showAlert("warning", "Missing!", "Please select an application zip file to install.", divInstallAppAlert); fileAppZip.trigger("focus"); return; } var formData = new FormData(); formData.append("fileAppZip", $("#fileAppZip")[0].files[0]); var btn = $("#btnInstallApp"); btn.button("loading"); HTTPRequest({ url: "api/apps/install?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), method: "POST", data: formData, contentType: false, processData: false, success: function (responseJSON) { $("#modalInstallApp").modal("hide"); var tableHtmlRow = getAppRowHtml(responseJSON.response.installedApp); $("#tableAppsBody").prepend(tableHtmlRow); updateAppsFooterCount(); showAlert("success", "App Installed!", "DNS application '" + appName + "' was installed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalInstallApp").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divInstallAppAlert }); } function updateApp() { var divUpdateAppAlert = $("#divUpdateAppAlert"); var appName = $("#txtUpdateApp").val(); var fileAppZip = $("#fileUpdateAppZip"); if (fileAppZip[0].files.length === 0) { showAlert("warning", "Missing!", "Please select an application zip file to update.", divUpdateAppAlert); fileAppZip.trigger("focus"); return; } var formData = new FormData(); formData.append("fileAppZip", $("#fileUpdateAppZip")[0].files[0]); var btn = $("#btnUpdateApp"); btn.button("loading"); HTTPRequest({ url: "api/apps/update?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), method: "POST", data: formData, contentType: false, processData: false, success: function (responseJSON) { $("#modalUpdateApp").modal("hide"); var tableHtmlRow = getAppRowHtml(responseJSON.response.updatedApp); var id = getAppRowId(responseJSON.response.updatedApp.name); $("#trApp" + id).replaceWith(tableHtmlRow); showAlert("success", "App Updated!", "DNS application '" + appName + "' was updated successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalUpdateApp").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divUpdateAppAlert }); } function uninstallApp(objBtn, appName) { if (!confirm("Are you sure you want to uninstall the DNS application '" + appName + "'?")) return; var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/apps/uninstall?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), success: function (responseJSON) { var id = btn.attr("data-id"); $("#trApp" + id).remove(); updateAppsFooterCount(); showAlert("success", "App Uninstalled!", "DNS application '" + appName + "' was uninstalled successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { showPageLogin(); } }); } function updateAppsFooterCount() { var totalApps = $("#tableApps >tbody >tr").length; if (totalApps > 0) $("#tableAppsFooter").html("Total Apps: " + totalApps + ""); else $("#tableAppsFooter").html("No App Found"); } function showAppConfigModal(objBtn, appName) { var node = getPrimaryClusterNodeName(); //always reading app config from primary node to avoid issues due to config propagation delays var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/apps/config/get?token=" + sessionData.token + "&name=" + encodeURIComponent(appName) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#divAppConfigAlert").html(""); $("#lblAppConfigName").html(appName); $("#txtAppConfig").val(responseJSON.response.config); $("#btnAppConfig").button("reset"); $("#modalAppConfig").modal("show"); setTimeout(function () { $("#txtAppConfig").trigger("focus"); }, 1000); }, error: function () { btn.button("reset"); }, invalidToken: function () { showPageLogin(); } }); } function saveAppConfig() { var divAppConfigAlert = $("#divAppConfigAlert"); var appName = $("#lblAppConfigName").text(); var config = $("#txtAppConfig").val(); var btn = $("#btnAppConfig"); btn.button("loading"); HTTPRequest({ url: "api/apps/config/set?token=" + sessionData.token + "&name=" + encodeURIComponent(appName), method: "POST", data: "config=" + encodeURIComponent(config), processData: false, success: function (responseJSON) { $("#modalAppConfig").modal("hide"); showAlert("success", "App Config Saved!", "The DNS application '" + appName + "' config was saved and reloaded successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalAppConfig").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divAppConfigAlert }); } ================================================ FILE: DnsServerCore/www/js/auth.js ================================================ /* 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 . */ var sessionData = null; $(function () { var token = localStorage.getItem("token"); if (token == null) { showPageLogin(); login("admin", "admin"); } else { HTTPRequest({ url: "api/user/session/get?token=" + token, success: function (responseJSON) { sessionData = responseJSON; localStorage.setItem("token", sessionData.token); $("#mnuUserDisplayName").text(sessionData.displayName); document.title = sessionData.info.dnsServerDomain + " - " + "Technitium DNS Server v" + sessionData.info.version; $("#lblAboutVersion").text(sessionData.info.version); $("#lblAboutUptime").text(moment(sessionData.info.uptimestamp).local().format("lll") + " (" + moment(sessionData.info.uptimestamp).fromNow() + ")"); $("#lblDnsServerDomain").text(" - " + sessionData.info.dnsServerDomain); $("#chkUseSoaSerialDateScheme").prop("checked", sessionData.info.useSoaSerialDateScheme); $("#chkDnssecValidation").prop("checked", sessionData.info.dnssecValidation); showPageMain(); }, error: function () { showPageLogin(); } }); } $("#optGroupDetailsUserList").on("change", function () { var selectedUser = $("#optGroupDetailsUserList").val(); switch (selectedUser) { case "blank": break; case "none": $("#txtGroupDetailsMembers").val(""); break; default: var existingUsers = $("#txtGroupDetailsMembers").val(); var existingUsersArray = existingUsers.split("\n"); var found = false; for (var i = 0; i < existingUsersArray.length; i++) { if (existingUsersArray[i] === selectedUser) { found = true; break; } } if (!found) { if ((existingUsers.length > 0) && !existingUsers.endsWith("\n")) existingUsers += "\n"; existingUsers += selectedUser + "\n"; $("#txtGroupDetailsMembers").val(existingUsers); } break; } }); $("#optUserDetailsGroupList").on("change", function () { var selectedGroup = $("#optUserDetailsGroupList").val(); switch (selectedGroup) { case "blank": break; case "none": $("#txtUserDetailsMemberOf").val(""); break; default: var existingGroups = $("#txtUserDetailsMemberOf").val(); var existingGroupsArray = existingGroups.split("\n"); var found = false; for (var i = 0; i < existingGroupsArray.length; i++) { if (existingGroupsArray[i] === selectedGroup) { found = true; break; } } if (!found) { if ((existingGroups.length > 0) && !existingGroups.endsWith("\n")) existingGroups += "\n"; existingGroups += selectedGroup + "\n"; $("#txtUserDetailsMemberOf").val(existingGroups); } break; } }); $("#optEditPermissionsUserList").on("change", function () { var selectedUser = $("#optEditPermissionsUserList").val(); switch (selectedUser) { case "blank": break; case "none": $("#tbodyEditPermissionsUser").html(""); break; default: var data = serializeTableData($("#tableEditPermissionsUser"), 4); var parts = data.split("|"); var found = false; for (var i = 0; i < parts.length; i += 4) { if (parts[i] === selectedUser) { found = true; break; } } if (!found) addEditPermissionUserRow(null, selectedUser, false, false, false); break; } }); $("#optEditPermissionsGroupList").on("change", function () { var selectedGroup = $("#optEditPermissionsGroupList").val(); switch (selectedGroup) { case "blank": break; case "none": $("#tbodyEditPermissionsGroup").html(""); break; default: var data = serializeTableData($("#tableEditPermissionsGroup"), 4); var parts = data.split("|"); var found = false; for (var i = 0; i < parts.length; i += 4) { if (parts[i] === selectedGroup) { found = true; break; } } if (!found) addEditPermissionGroupRow(null, selectedGroup, false, false, false); break; } }); }); function login(username, password) { var autoLogin = false; if (username == null) { username = $("#txtUser").val().toLowerCase(); password = $("#txtPass").val(); } else { autoLogin = true; } if ((username === null) || (username === "")) { showAlert("warning", "Missing!", "Please enter an username."); $("#txtUser").trigger("focus"); return; } if ((password === null) || (password === "")) { showAlert("warning", "Missing!", "Please enter a password."); $("#txtPass").trigger("focus"); return; } var totp = $("#txt2FATOTP").val(); if ($("#div2FAOTP").is(":visible")) { if ((totp == null) || (totp.length != 6)) { showAlert("warning", "Missing!", "Please enter the 6-digit OTP that you see in your authenticator app."); $("#txt2FATOTP").trigger("focus"); return; } } var btn = $("#btnLogin").button("loading"); HTTPRequest({ url: "api/user/login", method: "POST", data: "user=" + encodeURIComponent(username) + "&pass=" + encodeURIComponent(password) + "&totp=" + encodeURIComponent(totp) + "&includeInfo=true", procecssData: false, success: function (responseJSON) { sessionData = responseJSON; localStorage.setItem("token", sessionData.token); $("#mnuUserDisplayName").text(sessionData.displayName); document.title = sessionData.info.dnsServerDomain + " - " + "Technitium DNS Server v" + sessionData.info.version; $("#lblAboutVersion").text(sessionData.info.version); $("#lblAboutUptime").text(moment(sessionData.info.uptimestamp).local().format("lll") + " (" + moment(sessionData.info.uptimestamp).fromNow() + ")"); $("#lblDnsServerDomain").text(" - " + sessionData.info.dnsServerDomain); showPageMain(); if (!sessionData.totpEnabled && (username === "admin") && (password === "admin")) showChangePasswordModal(password); }, error: function () { btn.button("reset"); if ($("#div2FAOTP").is(":visible")) { $("#txt2FATOTP").val(""); $("#txt2FATOTP").trigger("focus"); } else { $("#txtUser").trigger("focus"); } if (autoLogin) hideAlert(); }, twoFactorAuthRequired: function () { btn.button("reset"); if (autoLogin) { $("#txtUser").trigger("focus"); } else { $("#txtPass").prop("disabled", true); $("#div2FAOTP").show(); $("#txt2FATOTP").trigger("focus"); } } }); } function logout() { HTTPRequest({ url: "api/user/logout?token=" + sessionData.token, success: function (responseJSON) { sessionData = null; showPageLogin(); }, error: function () { sessionData = null; showPageLogin(); } }); } function showCreateMyApiTokenModal() { $("#divCreateApiTokenAlert").html(""); $("#txtCreateApiTokenUsername").val(sessionData.username); $("#txtCreateApiTokenPassword").val(""); $("#txtCreateApiToken2FATOTP").val(""); $("#txtCreateApiTokenName").val(""); $("#txtCreateApiTokenUsername").show(); $("#optCreateApiTokenUsername").hide(); $("#divCreateApiTokenPassword").show(); if (sessionData.totpEnabled) $("#divCreateApiToken2FAOTP").show(); else $("#divCreateApiToken2FAOTP").hide(); $("#divCreateApiTokenLoader").hide(); $("#divCreateApiTokenForm").show(); $("#divCreateApiTokenOutput").hide(); var btnCreateApiToken = $("#btnCreateApiToken"); btnCreateApiToken.attr("onclick", "createMyApiToken(this); return false;"); btnCreateApiToken.show(); $("#modalCreateApiToken").modal("show"); setTimeout(function () { $("#txtCreateApiTokenPassword").trigger("focus"); }, 1000); } function createMyApiToken(objBtn) { var btn = $(objBtn); var divCreateApiTokenAlert = $("#divCreateApiTokenAlert"); var user = $("#txtCreateApiTokenUsername").val(); var password = $("#txtCreateApiTokenPassword").val(); var totp = $("#txtCreateApiToken2FATOTP").val(); var tokenName = $("#txtCreateApiTokenName").val(); if (password === "") { showAlert("warning", "Missing!", "Please enter a password.", divCreateApiTokenAlert); $("#txtCreateApiTokenPassword").trigger("focus"); return; } if (sessionData.totpEnabled) { if (totp.length != 6) { showAlert("warning", "Missing!", "Please enter the 6-digit OTP that you see in your authenticator app.", divCreateApiTokenAlert); $("#txtCreateApiToken2FATOTP").trigger("focus"); return; } } if (tokenName === "") { showAlert("warning", "Missing!", "Please enter a token name.", divCreateApiTokenAlert); $("#txtCreateApiTokenName").trigger("focus"); return; } btn.button("loading"); HTTPRequest({ url: "api/user/createToken", method: "POST", data: "user=" + encodeURIComponent(user) + "&pass=" + encodeURIComponent(password) + "&totp=" + encodeURIComponent(totp) + "&tokenName=" + encodeURIComponent(tokenName), processData: false, success: function (responseJSON) { btn.button("reset"); btn.hide(); $("#lblCreateApiTokenOutputUsername").text(responseJSON.username); $("#lblCreateApiTokenOutputTokenName").text(responseJSON.tokenName); $("#lblCreateApiTokenOutputToken").text(responseJSON.token); $("#divCreateApiTokenForm").hide(); $("#divCreateApiTokenOutput").show(); showAlert("success", "Token Created!", "API token was created successfully.", divCreateApiTokenAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalCreateApiToken").hide(""); showPageLogin(); }, objAlertPlaceholder: divCreateApiTokenAlert }); } function showChangePasswordModal(currentPassword) { $("#titleChangePassword").text("Change Password"); hideAlert($("#divChangePasswordAlert")); $("#txtChangePasswordUsername").val(sessionData.username); var txtChangePasswordCurrentPassword = $("#txtChangePasswordCurrentPassword"); if (currentPassword == null) { txtChangePasswordCurrentPassword.val(""); txtChangePasswordCurrentPassword.prop("disabled", false); } else { txtChangePasswordCurrentPassword.val(currentPassword); txtChangePasswordCurrentPassword.prop("disabled", true); } $("#divChangePasswordCurrentPassword").show(); $("#txtChangePasswordNewPassword").val(""); $("#txtChangePasswordConfirmPassword").val(""); $("#txtChangePassword2FATOTP").val(""); if (sessionData.totpEnabled) $("#divChangePassword2FATOTP").show(); else $("#divChangePassword2FATOTP").hide(); var btnChangePassword = $("#btnChangePassword"); btnChangePassword.text("Change"); btnChangePassword.attr("onclick", "changePassword(this); return false;"); btnChangePassword.show(); $("#modalChangePassword").modal("show"); setTimeout(function () { if (currentPassword == null) $("#txtChangePasswordCurrentPassword").trigger("focus"); else $("#txtChangePasswordNewPassword").trigger("focus"); }, 1000); } function changePassword(objBtn) { var btn = $(objBtn); var divChangePasswordAlert = $("#divChangePasswordAlert"); var password = $("#txtChangePasswordCurrentPassword").val(); var newPassword = $("#txtChangePasswordNewPassword").val(); var confirmPassword = $("#txtChangePasswordConfirmPassword").val(); var totp = $("#txtChangePassword2FATOTP").val(); if ((password === null) || (password === "")) { showAlert("warning", "Missing!", "Please enter the current password.", divChangePasswordAlert); $("#txtChangePasswordCurrentPassword").trigger("focus"); return; } if ((newPassword === null) || (newPassword === "")) { showAlert("warning", "Missing!", "Please enter new password.", divChangePasswordAlert); $("#txtChangePasswordNewPassword").trigger("focus"); return; } if ((confirmPassword === null) || (confirmPassword === "")) { showAlert("warning", "Missing!", "Please enter confirm password.", divChangePasswordAlert); $("#txtChangePasswordConfirmPassword").trigger("focus"); return; } if (newPassword !== confirmPassword) { showAlert("warning", "Mismatch!", "Passwords do not match. Please try again.", divChangePasswordAlert); $("#txtChangePasswordNewPassword").trigger("focus"); return; } if (sessionData.totpEnabled) { if ((totp == null) || (totp.length != 6)) { showAlert("warning", "Missing!", "Please enter the 6-digit OTP that you see in your authenticator app.", divChangePasswordAlert); $("#txtChangePassword2FATOTP").trigger("focus"); return; } } btn.button("loading"); HTTPRequest({ url: "api/user/changePassword", method: "POST", data: "token=" + sessionData.token + "&pass=" + encodeURIComponent(password) + "&newPass=" + encodeURIComponent(newPassword) + "&totp=" + encodeURIComponent(totp), processData: false, success: function (responseJSON) { $("#modalChangePassword").modal("hide"); $("#txtChangePasswordCurrentPassword").val(""); $("#txtChangePasswordNewPassword").val(""); $("#txtChangePasswordConfirmPassword").val(""); $("#txtChangePassword2FATOTP").val(""); btn.button("reset"); showAlert("success", "Password Changed!", "Password was changed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalChangePassword").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divChangePasswordAlert }); } function showConfigure2FAModal() { var divConfigure2FAAlert = $("#divConfigure2FAAlert"); var divConfigure2FALoader = $("#divConfigure2FALoader"); var divConfigure2FAViewer = $("#divConfigure2FAViewer"); var btnEnable2FA = $("#btnEnable2FA"); var btnDisable2FA = $("#btnDisable2FA"); divConfigure2FALoader.show(); divConfigure2FAViewer.hide(); btnEnable2FA.hide(); btnDisable2FA.hide(); var modalConfigure2FA = $("#modalConfigure2FA"); modalConfigure2FA.modal("show"); HTTPRequest({ url: "api/user/2fa/init?token=" + sessionData.token, success: function (responseJSON) { $("#txtConfigure2FAUsername").val(sessionData.username); $("#lblConfigure2FAStatus").text(responseJSON.response.totpEnabled ? "Enabled" : "Disabled"); if (responseJSON.response.totpEnabled) { $("#divConfigure2FAInitialize").hide(); divConfigure2FALoader.hide(); divConfigure2FAViewer.show(); btnDisable2FA.show(); } else { var secret = ""; for (var i = 0; i < responseJSON.response.secret.length; i++) { if ((i > 0) && (i % 4) == 0) secret += " "; secret += responseJSON.response.secret.substring(i, i + 1); } $("#lblConfigure2FAQRCode").html(""); $("#lblConfigure2FASecret").text(secret); $("#txtConfigure2FATOTP").val(""); $("#divConfigure2FAInitialize").show(); divConfigure2FALoader.hide(); divConfigure2FAViewer.show(); btnEnable2FA.show(); setTimeout(function () { $("#txtConfigure2FATOTP").trigger("focus"); }, 1000); } }, error: function () { divConfigure2FALoader.hide(); }, invalidToken: function () { modalConfigure2FA.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divConfigure2FAAlert, objLoaderPlaceholder: divConfigure2FALoader }); } function enable2FA(objBtn) { var btn = $(objBtn); var divConfigure2FAAlert = $("#divConfigure2FAAlert"); var totp = $("#txtConfigure2FATOTP").val(); if ((totp == null) || (totp.length != 6)) { showAlert("warning", "Missing!", "Please enter the 6-digit OTP that you see in your authenticator app.", divConfigure2FAAlert); $("#txtConfigure2FATOTP").trigger("focus"); return; } btn.button("loading"); HTTPRequest({ url: "api/user/2fa/enable?token=" + sessionData.token + "&totp=" + encodeURIComponent(totp), success: function (responseJSON) { sessionData.totpEnabled = true; $("#modalConfigure2FA").modal("hide"); btn.button("reset"); showAlert("success", "2FA Enabled!", "Two-factor authentication (2FA) was enabled successfully."); }, error: function () { btn.button("reset"); $("#txtConfigure2FATOTP").val(""); $("#txtConfigure2FATOTP").trigger("focus"); }, invalidToken: function () { btn.button("reset"); $("#modalConfigure2FA").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divConfigure2FAAlert }); } function disable2FA(objBtn) { if (!confirm("Are you sure you want to disable Two-factor authentication (2FA) ?")) return; var btn = $(objBtn); var divConfigure2FAAlert = $("#divConfigure2FAAlert"); btn.button("loading"); HTTPRequest({ url: "api/user/2fa/disable?token=" + sessionData.token, success: function (responseJSON) { sessionData.totpEnabled = false; $("#modalConfigure2FA").modal("hide"); btn.button("reset"); showAlert("success", "2FA Disabled!", "Two-factor authentication (2FA) was disabled successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalConfigure2FA").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divConfigure2FAAlert }); } function showMyProfileModal() { var divMyProfileAlert = $("#divMyProfileAlert"); var divMyProfileLoader = $("#divMyProfileLoader"); var divMyProfileViewer = $("#divMyProfileViewer"); divMyProfileLoader.show(); divMyProfileViewer.hide(); var modalMyProfile = $("#modalMyProfile"); modalMyProfile.modal("show"); HTTPRequest({ url: "api/user/profile/get?token=" + sessionData.token, success: function (responseJSON) { sessionData.displayName = responseJSON.response.displayName; sessionData.username = responseJSON.response.username; sessionData.totpEnabled = responseJSON.response.totpEnabled; $("#mnuUserDisplayName").text(sessionData.displayName); $("#txtMyProfileDisplayName").val(responseJSON.response.displayName); $("#txtMyProfileUsername").val(responseJSON.response.username); $("#lblMyProfile2FAStatus").text(responseJSON.response.totpEnabled ? "Enabled" : "Disabled"); $("#txtMyProfileSessionTimeout").val(responseJSON.response.sessionTimeoutSeconds); { var groupHtmlRows = ""; for (var i = 0; i < responseJSON.response.memberOfGroups.length; i++) { groupHtmlRows += "" + htmlEncode(responseJSON.response.memberOfGroups[i]) + ""; } $("#tbodyMyProfileMemberOf").html(groupHtmlRows); $("#tfootMyProfileMemberOf").html("Total Groups: " + responseJSON.response.memberOfGroups.length); } { var sessionHtmlRows = ""; for (var i = 0; i < responseJSON.response.sessions.length; i++) { var session; if (responseJSON.response.sessions[i].tokenName == null) session = htmlEncode("[" + responseJSON.response.sessions[i].partialToken + "]"); else session = htmlEncode(responseJSON.response.sessions[i].tokenName) + "
[" + htmlEncode(responseJSON.response.sessions[i].partialToken) + "]"; if (responseJSON.response.sessions[i].isCurrentSession) session += "
(current)"; switch (responseJSON.response.sessions[i].type) { case "Standard": session += "
Standard"; break; case "ApiToken": session += "
API Token"; break; default: session += "
Unknown"; break; } sessionHtmlRows += "" + session + "" + htmlEncode(moment(responseJSON.response.sessions[i].lastSeen).local().format("YYYY-MM-DD HH:mm:ss")) + "
" + htmlEncode("(" + moment(responseJSON.response.sessions[i].lastSeen).fromNow() + ")") + "" + htmlEncode(responseJSON.response.sessions[i].lastSeenRemoteAddress) + "" + htmlEncode(responseJSON.response.sessions[i].lastSeenUserAgent); sessionHtmlRows += "
"; } $("#tbodyMyProfileActiveSessions").html(sessionHtmlRows); $("#tfootMyProfileActiveSessions").html("Total Sessions: " + responseJSON.response.sessions.length); } divMyProfileLoader.hide(); divMyProfileViewer.show(); setTimeout(function () { $("#txtMyProfileDisplayName").trigger("focus"); }, 1000); }, error: function () { divMyProfileLoader.hide(); }, invalidToken: function () { modalMyProfile.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divMyProfileAlert, objLoaderPlaceholder: divMyProfileLoader }); } function saveMyProfile(objBtn) { var btn = $(objBtn); var divMyProfileAlert = $("#divMyProfileAlert"); var displayName = $("#txtMyProfileDisplayName").val(); var sessionTimeoutSeconds = $("#txtMyProfileSessionTimeout").val(); if (sessionTimeoutSeconds === "") sessionTimeoutSeconds = 1800; var apiUrl = "api/user/profile/set?token=" + sessionData.token + "&displayName=" + encodeURIComponent(displayName) + "&sessionTimeoutSeconds=" + encodeURIComponent(sessionTimeoutSeconds); btn.button("loading"); HTTPRequest({ url: apiUrl, success: function (responseJSON) { sessionData.displayName = responseJSON.response.displayName; $("#mnuUserDisplayName").text(sessionData.displayName); btn.button("reset"); $("#modalMyProfile").modal("hide"); showAlert("success", "Profile Saved!", "User profile was saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalMyProfile").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divMyProfileAlert }); } function deleteMySession(objMenuItem) { var divMyProfileAlert = $("#divMyProfileAlert"); var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var sessionType = mnuItem.attr("data-session-type"); var partialToken = mnuItem.attr("data-partial-token"); if (!confirm("Are you sure you want to delete the session [" + partialToken + "] ?")) return; var apiUrl = "api/user/session/delete?token=" + sessionData.token + "&partialToken=" + encodeURIComponent(partialToken); if (sessionType == "ApiToken") apiUrl += "&node=" + encodeURIComponent(getPrimaryClusterNodeName()); var btn = $("#btnMyProfileActiveSessionRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: apiUrl, success: function (responseJSON) { $("#trMyProfileActiveSessions" + id).remove(); var totalSessions = $('#tableMyProfileActiveSessions >tbody >tr').length; $("#tfootMyProfileActiveSessions").html("Total Sessions: " + totalSessions); showAlert("success", "Session Deleted!", "The user session was deleted successfully.", divMyProfileAlert); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalMyProfile").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divMyProfileAlert }); } function refreshAdminTab() { if ($("#adminTabListSessions").hasClass("active")) refreshAdminSessions(); else if ($("#adminTabListUsers").hasClass("active")) refreshAdminUsers(); else if ($("#adminTabListGroups").hasClass("active")) refreshAdminGroups(); else if ($("#adminTabListPermissions").hasClass("active")) refreshAdminPermissions(); else if ($("#adminTabListCluster").hasClass("active")) refreshAdminCluster(); else refreshAdminSessions(); } function refreshAdminSessions() { var divAdminSessionsLoader = $("#divAdminSessionsLoader"); var divAdminSessionsView = $("#divAdminSessionsView"); var node = $("#optAdminSessionsClusterNode").val(); divAdminSessionsLoader.show(); divAdminSessionsView.hide(); HTTPRequest({ url: "api/admin/sessions/list?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var tableHtmlRows = ""; for (var i = 0; i < responseJSON.response.sessions.length; i++) { var session; if (responseJSON.response.sessions[i].tokenName == null) session = "[" + htmlEncode(responseJSON.response.sessions[i].partialToken) + "]"; else session = htmlEncode(responseJSON.response.sessions[i].tokenName) + "
[" + htmlEncode(responseJSON.response.sessions[i].partialToken) + "]"; if (responseJSON.response.sessions[i].isCurrentSession) session += "
(current)"; switch (responseJSON.response.sessions[i].type) { case "Standard": session += "
Standard"; break; case "ApiToken": session += "
API Token"; break; default: session += "
Unknown"; break; } tableHtmlRows += "" + htmlEncode(responseJSON.response.sessions[i].username) + "" + session + "" + htmlEncode(moment(responseJSON.response.sessions[i].lastSeen).local().format("YYYY-MM-DD HH:mm:ss")) + "
" + htmlEncode("(" + moment(responseJSON.response.sessions[i].lastSeen).fromNow() + ")") + "" + htmlEncode(responseJSON.response.sessions[i].lastSeenRemoteAddress) + "" + htmlEncode(responseJSON.response.sessions[i].lastSeenUserAgent); tableHtmlRows += "
"; } var primaryNodeName = getPrimaryClusterNodeName(); if ((primaryNodeName == "") || (primaryNodeName == responseJSON.server)) $("#btnAdminSessionsCreateToken").show(); else $("#btnAdminSessionsCreateToken").hide(); $("#tbodyAdminSessions").html(tableHtmlRows); $("#tfootAdminSessions").html("Total Sessions: " + responseJSON.response.sessions.length); divAdminSessionsLoader.hide(); divAdminSessionsView.show(); }, error: function () { divAdminSessionsLoader.hide(); divAdminSessionsView.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divAdminSessionsLoader }); } function showCreateApiTokenModal() { var divCreateApiTokenAlert = $("#divCreateApiTokenAlert"); var divCreateApiTokenLoader = $("#divCreateApiTokenLoader"); var divCreateApiTokenForm = $("#divCreateApiTokenForm"); var divCreateApiTokenOutput = $("#divCreateApiTokenOutput"); divCreateApiTokenLoader.show(); divCreateApiTokenForm.hide(); divCreateApiTokenOutput.hide(); var btnCreateApiToken = $("#btnCreateApiToken"); btnCreateApiToken.attr("onclick", "createApiToken(this); return false;"); btnCreateApiToken.show(); var modalCreateApiToken = $("#modalCreateApiToken"); modalCreateApiToken.modal("show"); HTTPRequest({ url: "api/admin/users/list?token=" + sessionData.token, success: function (responseJSON) { var userListHtml = ""; for (var i = 0; i < responseJSON.response.users.length; i++) { userListHtml += ""; } $("#optCreateApiTokenUsername").html(userListHtml); $("#optCreateApiTokenUsername").show(); $("#txtCreateApiTokenUsername").hide(); $("#divCreateApiTokenPassword").hide(); $("#divCreateApiToken2FAOTP").hide(); $("#txtCreateApiTokenName").val(""); divCreateApiTokenLoader.hide(); divCreateApiTokenForm.show(); setTimeout(function () { $("#optCreateApiTokenUsername").trigger("focus"); }, 1000); }, error: function () { divCreateApiTokenLoader.hide(); }, invalidToken: function () { modalCreateApiToken.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divCreateApiTokenAlert, objLoaderPlaceholder: divCreateApiTokenLoader }); } function createApiToken(objBtn) { var btn = $(objBtn); var divCreateApiTokenAlert = $("#divCreateApiTokenAlert"); var user = $("#optCreateApiTokenUsername").val(); var tokenName = $("#txtCreateApiTokenName").val(); if (user === "") { showAlert("warning", "Missing!", "Please select a username.", divCreateApiTokenAlert); $("#optCreateApiTokenUsername").trigger("focus"); return; } if (tokenName === "") { showAlert("warning", "Missing!", "Please enter a token name.", divCreateApiTokenAlert); $("#txtCreateApiTokenName").trigger("focus"); return; } btn.button("loading"); HTTPRequest({ url: "api/admin/sessions/createToken?token=" + sessionData.token + "&user=" + encodeURIComponent(user) + "&tokenName=" + encodeURIComponent(tokenName), success: function (responseJSON) { btn.button("reset"); btn.hide(); $("#lblCreateApiTokenOutputUsername").text(responseJSON.response.username); $("#lblCreateApiTokenOutputTokenName").text(responseJSON.response.tokenName); $("#lblCreateApiTokenOutputToken").text(responseJSON.response.token); $("#divCreateApiTokenForm").hide(); $("#divCreateApiTokenOutput").show(); showAlert("success", "Token Created!", "API token was created successfully.", divCreateApiTokenAlert); refreshAdminSessions(); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalCreateApiToken").hide(""); showPageLogin(); }, objAlertPlaceholder: divCreateApiTokenAlert }); } function deleteAdminSession(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var sessionType = mnuItem.attr("data-session-type"); var partialToken = mnuItem.attr("data-partial-token"); if (!confirm("Are you sure you want to delete the session [" + partialToken + "] ?")) return; var apiUrl = "api/admin/sessions/delete?token=" + sessionData.token + "&partialToken=" + encodeURIComponent(partialToken); if (sessionType == "ApiToken") apiUrl += "&node=" + encodeURIComponent(getPrimaryClusterNodeName()); else apiUrl += "&node=" + encodeURIComponent($("#optAdminSessionsClusterNode").val()); var btn = $("#btnAdminSessionRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: apiUrl, success: function (responseJSON) { $("#trAdminSessions" + id).remove(); var totalSessions = $('#tableAdminSessions >tbody >tr').length; $("#tfootAdminSessions").html("Total Sessions: " + totalSessions); showAlert("success", "Session Deleted!", "The user session was deleted successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function refreshAdminUsers() { var divAdminUsersLoader = $("#divAdminUsersLoader"); var divAdminUsersView = $("#divAdminUsersView"); divAdminUsersLoader.show(); divAdminUsersView.hide(); HTTPRequest({ url: "api/admin/users/list?token=" + sessionData.token, success: function (responseJSON) { var tableHtmlRows = ""; for (var i = 0; i < responseJSON.response.users.length; i++) { tableHtmlRows += getAdminUsersRowHtml(i, responseJSON.response.users[i]); } $("#tbodyAdminUsers").html(tableHtmlRows); $("#tfootAdminUsers").html("Total Users: " + responseJSON.response.users.length); divAdminUsersLoader.hide(); divAdminUsersView.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divAdminUsersLoader }); } function getAdminUsersRowHtml(id, user) { var totpStatus = ""; if (user.totpEnabled) totpStatus += "Enabled"; else totpStatus += "Disabled"; var status = ""; if (user.disabled) status += "Disabled"; else status += "Enabled"; var tableHtmlRows = "" + htmlEncode(user.username) + "" + htmlEncode(user.displayName) + "" + totpStatus + "" + status + "" + htmlEncode(moment(user.recentSessionLoggedOn).local().format("YYYY-MM-DD HH:mm:ss")) + " from " + htmlEncode(user.recentSessionRemoteAddress) + "" + htmlEncode(moment(user.previousSessionLoggedOn).local().format("YYYY-MM-DD HH:mm:ss")) + " from " + htmlEncode(user.previousSessionRemoteAddress); tableHtmlRows += "
"; return tableHtmlRows; } function showAddUserModal() { $("#divAddUserAlert").html(""); $("#txtAddUserDisplayName").val(""); $("#txtAddUserUsername").val(""); $("#txtAddUserPassword").val(""); $("#txtAddUserConfirmPassword").val(""); $("#modalAddUser").modal("show"); setTimeout(function () { $("#txtAddUserDisplayName").trigger("focus"); }, 1000); } function addUser(objBtn) { var btn = $(objBtn); var divAddUserAlert = $("#divAddUserAlert"); var user = $("#txtAddUserUsername").val(); if (user === "") { showAlert("warning", "Missing!", "Please enter an username to add user.", divAddUserAlert); $("#txtAddUserUsername").trigger("focus"); return; } var pass = $("#txtAddUserPassword").val(); if (pass === "") { showAlert("warning", "Missing!", "Please enter a password to add user.", divAddUserAlert); $("#txtAddUserPassword").trigger("focus"); return; } var confirmPass = $("#txtAddUserConfirmPassword").val(); if (confirmPass === "") { showAlert("warning", "Missing!", "Please enter confirm password.", divAddUserAlert); $("#txtAddUserConfirmPassword").trigger("focus"); return; } if (pass !== confirmPass) { showAlert("warning", "Mismatch!", "Passwords do not match. Please try again.", divAddUserAlert); $("#txtAddUserConfirmPassword").trigger("focus"); return; } var displayName = $("#txtAddUserDisplayName").val(); btn.button("loading"); HTTPRequest({ url: "api/admin/users/create", method: "POST", data: "token=" + sessionData.token + "&displayName=" + encodeURIComponent(displayName) + "&user=" + encodeURIComponent(user) + "&pass=" + encodeURIComponent(pass), processData: false, success: function (responseJSON) { btn.button("reset"); $("#modalAddUser").modal("hide"); var id = Math.floor(Math.random() * 1000000); var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response); $("#tableAdminUsers").prepend(tableHtmlRow); var totalUsers = $('#tableAdminUsers >tbody >tr').length; $("#tfootAdminUsers").html("Total Users: " + totalUsers); showAlert("success", "User Added!", "User was added successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalAddUser").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divAddUserAlert }); } function showUserDetailsModal(objMenuItem) { var divUserDetailsAlert = $("#divUserDetailsAlert"); var divUserDetailsLoader = $("#divUserDetailsLoader"); var divUserDetailsViewer = $("#divUserDetailsViewer"); var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var username = mnuItem.attr("data-username"); divUserDetailsLoader.show(); divUserDetailsViewer.hide(); var modalUserDetails = $("#modalUserDetails"); modalUserDetails.modal("show"); HTTPRequest({ url: "api/admin/users/get?token=" + sessionData.token + "&user=" + encodeURIComponent(username) + "&includeGroups=true", success: function (responseJSON) { $("#txtUserDetailsDisplayName").val(responseJSON.response.displayName); $("#txtUserDetailsUsername").val(responseJSON.response.username); $("#lblUserDetails2FAStatus").text(responseJSON.response.totpEnabled ? "Enabled" : "Disabled"); $("#chkUserDetailsDisableAccount").prop("checked", responseJSON.response.disabled); $("#txtUserDetailsSessionTimeout").val(responseJSON.response.sessionTimeoutSeconds); var memberOf = ""; for (var i = 0; i < responseJSON.response.memberOfGroups.length; i++) { memberOf += htmlEncode(responseJSON.response.memberOfGroups[i]) + "\n"; } $("#txtUserDetailsMemberOf").val(memberOf); var groupListHtml = ""; for (var i = 0; i < responseJSON.response.groups.length; i++) { groupListHtml += ""; } $("#optUserDetailsGroupList").html(groupListHtml); var sessionHtmlRows = ""; for (var i = 0; i < responseJSON.response.sessions.length; i++) { var session; if (responseJSON.response.sessions[i].tokenName == null) session = htmlEncode("[" + responseJSON.response.sessions[i].partialToken + "]"); else session = htmlEncode(responseJSON.response.sessions[i].tokenName) + "
[" + htmlEncode(responseJSON.response.sessions[i].partialToken) + "]"; if (responseJSON.response.sessions[i].isCurrentSession) session += "
(current)"; switch (responseJSON.response.sessions[i].type) { case "Standard": session += "
Standard"; break; case "ApiToken": session += "
API Token"; break; default: session += "
Unknown"; break; } sessionHtmlRows += "" + session + "" + htmlEncode(moment(responseJSON.response.sessions[i].lastSeen).local().format("YYYY-MM-DD HH:mm:ss")) + "
" + htmlEncode("(" + moment(responseJSON.response.sessions[i].lastSeen).fromNow() + ")") + "" + htmlEncode(responseJSON.response.sessions[i].lastSeenRemoteAddress) + "" + htmlEncode(responseJSON.response.sessions[i].lastSeenUserAgent); sessionHtmlRows += "
"; } $("#tbodyUserDetailsActiveSessions").html(sessionHtmlRows); $("#tfootUserDetailsActiveSessions").html("Total Sessions: " + responseJSON.response.sessions.length); var btnUserDetailsSave = $("#btnUserDetailsSave"); btnUserDetailsSave.attr("data-id", id); btnUserDetailsSave.attr("data-username", username); divUserDetailsLoader.hide(); divUserDetailsViewer.show(); setTimeout(function () { $("#txtUserDetailsDisplayName").trigger("focus"); }, 1000); }, error: function () { divUserDetailsLoader.hide(); }, invalidToken: function () { modalUserDetails.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divUserDetailsAlert, objLoaderPlaceholder: divUserDetailsLoader }); } function deleteUserSession(objMenuItem) { var divUserDetailsAlert = $("#divUserDetailsAlert"); var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var sessionType = mnuItem.attr("data-session-type"); var partialToken = mnuItem.attr("data-partial-token"); if (!confirm("Are you sure you want to delete the session [" + partialToken + "] ?")) return; var apiUrl = "api/admin/sessions/delete?token=" + sessionData.token + "&partialToken=" + encodeURIComponent(partialToken); if (sessionType == "ApiToken") apiUrl += "&node=" + encodeURIComponent(getPrimaryClusterNodeName()); var btn = $("#btnUserDetailsActiveSessionRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: apiUrl, success: function (responseJSON) { $("#trUserDetailsActiveSessions" + id).remove(); var totalSessions = $('#tableUserDetailsActiveSessions >tbody >tr').length; $("#tfootUserDetailsActiveSessions").html("Total Sessions: " + totalSessions); showAlert("success", "Session Deleted!", "The user session was deleted successfully.", divUserDetailsAlert); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalUserDetails").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divUserDetailsAlert }); } function saveUserDetails(objBtn) { var btn = $(objBtn); var divUserDetailsAlert = $("#divUserDetailsAlert"); var id = btn.attr("data-id"); var username = btn.attr("data-username"); var newUsername = $("#txtUserDetailsUsername").val(); var displayName = $("#txtUserDetailsDisplayName").val(); var disabled = $("#chkUserDetailsDisableAccount").prop("checked"); var sessionTimeoutSeconds = $("#txtUserDetailsSessionTimeout").val(); if (sessionTimeoutSeconds === "") sessionTimeoutSeconds = 1800; var memberOfGroups = cleanTextList($("#txtUserDetailsMemberOf").val()); var apiUrl = "api/admin/users/set?token=" + sessionData.token + "&user=" + encodeURIComponent(username) + "&displayName=" + encodeURIComponent(displayName) + "&disabled=" + disabled + "&sessionTimeoutSeconds=" + encodeURIComponent(sessionTimeoutSeconds) + "&memberOfGroups=" + encodeURIComponent(memberOfGroups); if (newUsername !== username) apiUrl += "&newUser=" + encodeURIComponent(newUsername); btn.button("loading"); HTTPRequest({ url: apiUrl, success: function (responseJSON) { if (sessionData.username === username) { sessionData.displayName = responseJSON.response.displayName; sessionData.username = responseJSON.response.username; $("#mnuUserDisplayName").text(sessionData.displayName); } var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response); $("#trAdminUsers" + id).replaceWith(tableHtmlRow); btn.button("reset"); $("#modalUserDetails").modal("hide"); showAlert("success", "User Saved!", "User details were saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalUserDetails").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divUserDetailsAlert }); } function disableUser(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var username = mnuItem.attr("data-username"); if (!confirm("Are you sure you want to disable the user [" + username + "] account?")) return; var btn = $("#btnAdminUserRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/admin/users/set?token=" + sessionData.token + "&user=" + encodeURIComponent(username) + "&disabled=true", success: function (responseJSON) { var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response); $("#trAdminUsers" + id).replaceWith(tableHtmlRow); showAlert("success", "User Disabled!", "User [" + username + "] account was disabled successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function enableUser(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var username = mnuItem.attr("data-username"); var btn = $("#btnAdminUserRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/admin/users/set?token=" + sessionData.token + "&user=" + encodeURIComponent(username) + "&disabled=false", success: function (responseJSON) { var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response); $("#trAdminUsers" + id).replaceWith(tableHtmlRow); showAlert("success", "User Enabled!", "User [" + username + "] account was enabled successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function showResetUserPasswordModal(objMenuItem) { var mnuItem = $(objMenuItem); var username = mnuItem.attr("data-username"); $("#titleChangePassword").text("Reset Password"); hideAlert($("#divChangePasswordAlert")); $("#txtChangePasswordUsername").val(username); $("#divChangePasswordCurrentPassword").hide(); $("#txtChangePasswordNewPassword").val(""); $("#txtChangePasswordConfirmPassword").val(""); $("#divChangePassword2FATOTP").hide(); var btnChangePassword = $("#btnChangePassword"); btnChangePassword.text("Reset"); btnChangePassword.attr("onclick", "resetUserPassword(this); return false;"); btnChangePassword.show(); $("#modalChangePassword").modal("show"); setTimeout(function () { $("#txtChangePasswordNewPassword").trigger("focus"); }, 1000); } function resetUserPassword(objBtn) { var btn = $(objBtn); var divChangePasswordAlert = $("#divChangePasswordAlert"); var user = $("#txtChangePasswordUsername").val(); var newPassword = $("#txtChangePasswordNewPassword").val(); var confirmPassword = $("#txtChangePasswordConfirmPassword").val(); if (newPassword === "") { showAlert("warning", "Missing!", "Please enter new password.", divChangePasswordAlert); $("#txtChangePasswordNewPassword").trigger("focus"); return; } if (confirmPassword === "") { showAlert("warning", "Missing!", "Please enter confirm password.", divChangePasswordAlert); $("#txtChangePasswordConfirmPassword").trigger("focus"); return; } if (newPassword !== confirmPassword) { showAlert("warning", "Mismatch!", "Passwords do not match. Please try again.", divChangePasswordAlert); $("#txtChangePasswordNewPassword").trigger("focus"); return; } btn.button("loading"); HTTPRequest({ url: "api/admin/users/set", method: "POST", data: "token=" + sessionData.token + "&user=" + encodeURIComponent(user) + "&newPass=" + encodeURIComponent(newPassword), processData: false, success: function (responseJSON) { $("#modalChangePassword").modal("hide"); $("#txtChangePasswordCurrentPassword").val(""); $("#txtChangePasswordNewPassword").val(""); $("#txtChangePasswordConfirmPassword").val(""); $("#txtChangePassword2FATOTP").val(""); btn.button("reset"); showAlert("success", "Password Reset!", "Password was reset successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divChangePasswordAlert }); } function adminDisable2FA(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var username = mnuItem.attr("data-username"); if (!confirm("Are you sure you want to disable Two-factor authentication (2FA) for user [" + username + "] ?")) return; var btn = $("#btnAdminUserRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/admin/users/set?token=" + sessionData.token + "&user=" + encodeURIComponent(username) + "&totpEnabled=false", success: function (responseJSON) { if (username == sessionData.username) sessionData.totpEnabled = false; var tableHtmlRow = getAdminUsersRowHtml(id, responseJSON.response); $("#trAdminUsers" + id).replaceWith(tableHtmlRow); showAlert("success", "2FA Disabled!", "Two-factor authentication was disabled successfully for user [" + username + "]."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function deleteUser(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var username = mnuItem.attr("data-username"); if (!confirm("Are you sure you want to delete the user [" + username + "] account?")) return; var btn = $("#btnAdminUserRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/admin/users/delete?token=" + sessionData.token + "&user=" + encodeURIComponent(username), success: function (responseJSON) { $("#trAdminUsers" + id).remove(); var totalUsers = $('#tableAdminUsers >tbody >tr').length; $("#tfootAdminUsers").html("Total Users: " + totalUsers); showAlert("success", "User Deleted!", "User account was deleted successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function refreshAdminGroups() { var divAdminGroupsLoader = $("#divAdminGroupsLoader"); var divAdminGroupsView = $("#divAdminGroupsView"); divAdminGroupsLoader.show(); divAdminGroupsView.hide(); HTTPRequest({ url: "api/admin/groups/list?token=" + sessionData.token, success: function (responseJSON) { var tableHtmlRows = ""; for (var i = 0; i < responseJSON.response.groups.length; i++) { tableHtmlRows += getAdminGroupsRowHtml(i, responseJSON.response.groups[i]); } $("#tbodyAdminGroups").html(tableHtmlRows); $("#tfootAdminGroups").html("Total Groups: " + responseJSON.response.groups.length); divAdminGroupsLoader.hide(); divAdminGroupsView.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divAdminGroupsLoader }); } function getAdminGroupsRowHtml(id, group) { var tableHtmlRows = "" + htmlEncode(group.name) + "" + htmlEncode(group.description).replace(/\n/g, "
"); tableHtmlRows += "
"; return tableHtmlRows; } function showAddGroupModal() { $("#divAddGroupAlert").html(""); $("#txtAddGroupName").val(""); $("#txtAddGroupDescription").val(""); $("#modalAddGroup").modal("show"); setTimeout(function () { $("#txtAddGroupName").trigger("focus"); }, 1000); } function addGroup(objBtn) { var btn = $(objBtn); var divAddGroupAlert = $("#divAddGroupAlert"); var group = $("#txtAddGroupName").val(); if (group === "") { showAlert("warning", "Missing!", "Please enter a name to add group.", divAddGroupAlert); $("#txtAddGroupName").trigger("focus"); return; } var description = $("#txtAddGroupDescription").val(); btn.button("loading"); HTTPRequest({ url: "api/admin/groups/create?token=" + sessionData.token + "&group=" + encodeURIComponent(group) + "&description=" + encodeURIComponent(description), success: function (responseJSON) { btn.button("reset"); $("#modalAddGroup").modal("hide"); var id = Math.floor(Math.random() * 1000000); var tableHtmlRow = getAdminGroupsRowHtml(id, responseJSON.response); $("#tableAdminGroups").prepend(tableHtmlRow); var totalGroups = $('#tableAdminGroups >tbody >tr').length; $("#tfootAdminGroups").html("Total Groups: " + totalGroups); showAlert("success", "Group Added!", "Group was added successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalAddGroup").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divAddGroupAlert }); } function showGroupDetailsModal(objMenuItem) { var divGroupDetailsAlert = $("#divGroupDetailsAlert"); var divGroupDetailsLoader = $("#divGroupDetailsLoader"); var divGroupDetailsViewer = $("#divGroupDetailsViewer"); var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var group = mnuItem.attr("data-group"); divGroupDetailsLoader.show(); divGroupDetailsViewer.hide(); var modalGroupDetails = $("#modalGroupDetails"); modalGroupDetails.modal("show"); HTTPRequest({ url: "api/admin/groups/get?token=" + sessionData.token + "&group=" + encodeURIComponent(group) + "&includeUsers=true", success: function (responseJSON) { $("#txtGroupDetailsName").val(responseJSON.response.name); $("#txtGroupDetailsDescription").val(responseJSON.response.description); var members = ""; for (var i = 0; i < responseJSON.response.members.length; i++) { members += htmlEncode(responseJSON.response.members[i]) + "\n"; } $("#txtGroupDetailsMembers").val(members); var userListHtml = ""; for (var i = 0; i < responseJSON.response.users.length; i++) { userListHtml += ""; } $("#optGroupDetailsUserList").html(userListHtml); var btnGroupDetailsSave = $("#btnGroupDetailsSave"); btnGroupDetailsSave.attr("data-id", id); btnGroupDetailsSave.attr("data-group", group); divGroupDetailsLoader.hide(); divGroupDetailsViewer.show(); setTimeout(function () { $("#txtGroupDetailsName").trigger("focus"); }, 1000); }, error: function () { divGroupDetailsLoader.hide(); }, invalidToken: function () { modalGroupDetails.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divGroupDetailsAlert, objLoaderPlaceholder: divGroupDetailsLoader }); } function saveGroupDetails(objBtn) { var btn = $(objBtn); var divGroupDetailsAlert = $("#divGroupDetailsAlert"); var id = btn.attr("data-id"); var group = btn.attr("data-group"); var newGroup = $("#txtGroupDetailsName").val(); var description = $("#txtGroupDetailsDescription").val(); var members = cleanTextList($("#txtGroupDetailsMembers").val()); var apiUrl = "api/admin/groups/set?token=" + sessionData.token + "&group=" + encodeURIComponent(group) + "&description=" + encodeURIComponent(description) + "&members=" + encodeURIComponent(members); if (newGroup !== group) apiUrl += "&newGroup=" + encodeURIComponent(newGroup); btn.button("loading"); HTTPRequest({ url: apiUrl, success: function (responseJSON) { var tableHtmlRow = getAdminGroupsRowHtml(id, responseJSON.response); $("#trAdminGroups" + id).replaceWith(tableHtmlRow); btn.button("reset"); $("#modalGroupDetails").modal("hide"); showAlert("success", "Group Saved!", "Group details were saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalGroupDetails").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divGroupDetailsAlert }); } function deleteGroup(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var group = mnuItem.attr("data-group"); if (!confirm("Are you sure you want to delete the group [" + group + "] ?")) return; var btn = $("#btnAdminGroupRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/admin/groups/delete?token=" + sessionData.token + "&group=" + encodeURIComponent(group), success: function (responseJSON) { $("#trAdminGroups" + id).remove(); var totalGroups = $('#tableAdminGroups >tbody >tr').length; $("#tfootAdminGroups").html("Total Groups: " + totalGroups); showAlert("success", "Group Deleted!", "Group was deleted successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function refreshAdminPermissions() { var divAdminPermissionsLoader = $("#divAdminPermissionsLoader"); var divAdminPermissionsView = $("#divAdminPermissionsView"); divAdminPermissionsLoader.show(); divAdminPermissionsView.hide(); HTTPRequest({ url: "api/admin/permissions/list?token=" + sessionData.token, success: function (responseJSON) { var tableHtmlRows = ""; for (var i = 0; i < responseJSON.response.permissions.length; i++) { tableHtmlRows += getAdminPermissionsRowHtml(i, responseJSON.response.permissions[i]); } $("#tbodyAdminPermissions").html(tableHtmlRows); $("#tfootAdminPermissions").html("Total Sections: " + responseJSON.response.permissions.length); divAdminPermissionsLoader.hide(); divAdminPermissionsView.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divAdminPermissionsLoader }); } function getAdminPermissionsRowHtml(id, permission) { var userPermissionsHtml = ""; for (var i = 0; i < permission.userPermissions.length; i++) { userPermissionsHtml += ""; } userPermissionsHtml += ""; if (permission.userPermissions.length == 0) userPermissionsHtml += ""; userPermissionsHtml += "
UsernameViewModifyDelete
" + htmlEncode(permission.userPermissions[i].username) + "" + (permission.userPermissions[i].canView ? "" : "") + "" + (permission.userPermissions[i].canModify ? "" : "") + "" + (permission.userPermissions[i].canDelete ? "" : "") + "
No user permissions
"; var groupPermissionsHtml = ""; for (var i = 0; i < permission.groupPermissions.length; i++) { groupPermissionsHtml += ""; } groupPermissionsHtml += ""; if (permission.groupPermissions.length == 0) groupPermissionsHtml += ""; groupPermissionsHtml += "
GroupViewModifyDelete
" + htmlEncode(permission.groupPermissions[i].name) + "" + (permission.groupPermissions[i].canView ? "" : "") + "" + (permission.groupPermissions[i].canModify ? "" : "") + "" + (permission.groupPermissions[i].canDelete ? "" : "") + "
No group permissions
"; var tableHtmlRows = "" + htmlEncode(permission.section) + "" + userPermissionsHtml + "" + groupPermissionsHtml; tableHtmlRows += "
"; return tableHtmlRows; } function showEditSectionPermissionsModal(objMenuItem) { var divEditPermissionsAlert = $("#divEditPermissionsAlert"); var divEditPermissionsLoader = $("#divEditPermissionsLoader"); var divEditPermissionsViewer = $("#divEditPermissionsViewer"); var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var section = mnuItem.attr("data-section"); $("#lblEditPermissionsName").text(section); $("#tbodyEditPermissionsUser").html(""); $("#tbodyEditPermissionsGroup").html(""); divEditPermissionsLoader.show(); divEditPermissionsViewer.hide(); var btnEditPermissionsSave = $("#btnEditPermissionsSave"); btnEditPermissionsSave.attr("onclick", "saveSectionPermissions(this); return false;"); btnEditPermissionsSave.show(); var modalEditPermissions = $("#modalEditPermissions"); modalEditPermissions.modal("show"); HTTPRequest({ url: "api/admin/permissions/get?token=" + sessionData.token + "§ion=" + section + "&includeUsersAndGroups=true", success: function (responseJSON) { $("#lblEditPermissionsName").text(responseJSON.response.section); //user permissions for (var i = 0; i < responseJSON.response.userPermissions.length; i++) { addEditPermissionUserRow(i, responseJSON.response.userPermissions[i].username, responseJSON.response.userPermissions[i].canView, responseJSON.response.userPermissions[i].canModify, responseJSON.response.userPermissions[i].canDelete); } //load users list var userListHtml = ""; for (var i = 0; i < responseJSON.response.users.length; i++) { userListHtml += ""; } $("#optEditPermissionsUserList").html(userListHtml); //group permissions for (var i = 0; i < responseJSON.response.groupPermissions.length; i++) { addEditPermissionGroupRow(i, responseJSON.response.groupPermissions[i].name, responseJSON.response.groupPermissions[i].canView, responseJSON.response.groupPermissions[i].canModify, responseJSON.response.groupPermissions[i].canDelete); } //load groups list var groupListHtml = ""; for (var i = 0; i < responseJSON.response.groups.length; i++) { groupListHtml += ""; } $("#optEditPermissionsGroupList").html(groupListHtml); btnEditPermissionsSave.attr("data-id", id); btnEditPermissionsSave.attr("data-section", responseJSON.response.section); divEditPermissionsLoader.hide(); divEditPermissionsViewer.show(); }, error: function () { divEditPermissionsLoader.hide(); }, invalidToken: function () { modalEditPermissions.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditPermissionsAlert, objLoaderPlaceholder: divEditPermissionsLoader }); } function addEditPermissionUserRow(id, username, canView, canModify, canDelete) { if (id == null) id = Math.floor(Math.random() * 10000); var tableHtmlRow = "" + htmlEncode(username) + ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; $("#tbodyEditPermissionsUser").append(tableHtmlRow); } function addEditPermissionGroupRow(id, name, canView, canModify, canDelete) { if (id == null) id = Math.floor(Math.random() * 10000); var tableHtmlRow = "" + htmlEncode(name) + ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; $("#tbodyEditPermissionsGroup").append(tableHtmlRow); } function saveSectionPermissions(objBtn) { var btn = $(objBtn); var divEditPermissionsAlert = $("#divEditPermissionsAlert"); var id = btn.attr("data-id"); var section = btn.attr("data-section"); var userPermissions = serializeTableData($("#tableEditPermissionsUser"), 4); var groupPermissions = serializeTableData($("#tableEditPermissionsGroup"), 4); var apiUrl = "api/admin/permissions/set?token=" + sessionData.token + "§ion=" + encodeURIComponent(section) + "&userPermissions=" + encodeURIComponent(userPermissions) + "&groupPermissions=" + encodeURIComponent(groupPermissions) + "&node=" + encodeURIComponent(getPrimaryClusterNodeName()); btn.button("loading"); HTTPRequest({ url: apiUrl, success: function (responseJSON) { var tableHtmlRow = getAdminPermissionsRowHtml(id, responseJSON.response); $("#trAdminPermissions" + id).replaceWith(tableHtmlRow); btn.button("reset"); $("#modalEditPermissions").modal("hide"); showAlert("success", "Permissions Saved!", "Section permissions were saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalEditPermissions").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditPermissionsAlert }); } ================================================ FILE: DnsServerCore/www/js/cluster.js ================================================ /* 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 . */ $(function () { $("#optInitializeNewClusterQuickIpAddresses").on("change", function () { var selectedIpAddress = $("#optInitializeNewClusterQuickIpAddresses").val(); switch (selectedIpAddress) { case "blank": break; default: var existingList = $("#txtInitializeNewClusterPrimaryNodeIpAddresses").val(); if (existingList.indexOf(selectedIpAddress) < 0) { existingList += selectedIpAddress + "\n"; $("#txtInitializeNewClusterPrimaryNodeIpAddresses").val(existingList); } break; } }); $("#optInitializeJoinClusterQuickIpAddresses").on("change", function () { var selectedIpAddress = $("#optInitializeJoinClusterQuickIpAddresses").val(); switch (selectedIpAddress) { case "blank": break; default: var existingList = $("#txtInitializeJoinClusterSecondaryNodeIpAddresses").val(); if (existingList.indexOf(selectedIpAddress) < 0) { existingList += selectedIpAddress + "\n"; $("#txtInitializeJoinClusterSecondaryNodeIpAddresses").val(existingList); } break; } }); $("#optEditClusterNodeQuickSelfIpAddresses").on("change", function () { var selectedIpAddress = $("#optEditClusterNodeQuickSelfIpAddresses").val(); switch (selectedIpAddress) { case "blank": break; default: var existingList = $("#txtEditClusterNodeSelfNodeIpAddresses").val(); if (existingList.indexOf(selectedIpAddress) < 0) { existingList += selectedIpAddress + "\n"; $("#txtEditClusterNodeSelfNodeIpAddresses").val(existingList); } break; } }); }); function refreshAdminCluster() { var divAdminClusterLoader = $("#divAdminClusterLoader"); var divAdminClusterView = $("#divAdminClusterView"); var node = $("#optAdminClusterNode").val(); divAdminClusterLoader.show(); divAdminClusterView.hide(); HTTPRequest({ url: "api/admin/cluster/state?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { reloadAdminClusterView(responseJSON); divAdminClusterLoader.hide(); divAdminClusterView.show(); }, error: function () { divAdminClusterLoader.hide(); divAdminClusterView.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divAdminClusterLoader }); } function updateAdminClusterDataAndGui(responseJSON) { sessionData.info.dnsServerDomain = responseJSON.response.dnsServerDomain; sessionData.info.clusterDomain = responseJSON.response.clusterDomain; document.title = responseJSON.response.dnsServerDomain + " - " + "Technitium DNS Server v" + responseJSON.response.version; $("#lblAboutVersion").text(responseJSON.response.version); $("#lblDnsServerDomain").text(" - " + responseJSON.response.dnsServerDomain); } function reloadAdminClusterView(responseJSON) { sessionData.info.clusterInitialized = responseJSON.response.clusterInitialized; sessionData.info.clusterNodes = responseJSON.response.clusterNodes; updateAllClusterNodeDropDowns(); if (responseJSON.response.clusterInitialized) { var selfNodeType; for (var i = 0; i < responseJSON.response.clusterNodes.length; i++) { if (responseJSON.response.clusterNodes[i].state == "Self") { selfNodeType = responseJSON.response.clusterNodes[i].type; break; } } var tableHtmlRows = ""; for (var i = 0; i < responseJSON.response.clusterNodes.length; i++) { var ipAddresses = ""; var ipAddressesCsv = ""; for (var j = 0; j < responseJSON.response.clusterNodes[i].ipAddresses.length; j++) { ipAddresses += htmlEncode(responseJSON.response.clusterNodes[i].ipAddresses[j]) + "
"; if (ipAddressesCsv.length == 0) ipAddressesCsv = responseJSON.response.clusterNodes[i].ipAddresses[j]; else ipAddressesCsv += "," + responseJSON.response.clusterNodes[i].ipAddresses[j]; } var nodeType; switch (responseJSON.response.clusterNodes[i].type) { case "Primary": nodeType = "Primary"; break; case "Secondary": nodeType = "Secondary"; break; default: nodeType = "Unknown"; break; } var clusterNodestate; switch (responseJSON.response.clusterNodes[i].state) { case "Self": clusterNodestate = "Self"; break; case "Connected": clusterNodestate = "Connected"; break; case "Unreachable": clusterNodestate = "Unreachable"; break; default: clusterNodestate = "Unknown"; break; } var upSince = ""; if (responseJSON.response.clusterNodes[i].upSince != null) upSince = moment(responseJSON.response.clusterNodes[i].upSince).local().format("YYYY-MM-DD HH:mm") + "
(" + moment(responseJSON.response.clusterNodes[i].upSince).fromNow() + ")"; var lastSeen = ""; var lastSynced = ""; switch (responseJSON.response.clusterNodes[i].state) { case "Self": if (responseJSON.response.clusterNodes[i].type == "Secondary") { if (responseJSON.response.clusterNodes[i].configLastSynced != null) lastSynced = moment(responseJSON.response.clusterNodes[i].configLastSynced).local().format("YYYY-MM-DD HH:mm") + "
(" + moment(responseJSON.response.clusterNodes[i].configLastSynced).fromNow() + ")"; } break; default: if (responseJSON.response.clusterNodes[i].lastSeen != null) lastSeen = moment(responseJSON.response.clusterNodes[i].lastSeen).local().format("YYYY-MM-DD HH:mm") + "
(" + moment(responseJSON.response.clusterNodes[i].lastSeen).fromNow() + ")"; break; } tableHtmlRows += "" + htmlEncode(responseJSON.response.clusterNodes[i].name) + "" + ipAddresses + "" + htmlEncode(responseJSON.response.clusterNodes[i].url) + "" + nodeType + "" + clusterNodestate + "" + upSince + "" + lastSeen + "" + lastSynced; tableHtmlRows += ""; tableHtmlRows += ""; switch (selfNodeType) { case "Primary": tableHtmlRows += "
    "; if (responseJSON.response.clusterNodes[i].state == "Self") tableHtmlRows += "
  • Edit Node
  • "; if (responseJSON.response.clusterNodes[i].type == "Secondary") tableHtmlRows += "
  • Remove Node
  • "; tableHtmlRows += "
"; break; case "Secondary": if (responseJSON.response.clusterNodes[i].state == "Self") { tableHtmlRows += "
"; } else if (responseJSON.response.clusterNodes[i].type == "Primary") { tableHtmlRows += "
    "; tableHtmlRows += "
  • Edit Node
  • "; tableHtmlRows += "
"; } break; } tableHtmlRows += ""; tableHtmlRows += ""; } $("#divAdminClusterInitialize").hide(); switch (selfNodeType) { case "Primary": $("#btnClusterResync").hide(); $("#btnClusterOptions").show(); $("#btnClusterLeave").hide(); $("#btnClusterDelete").show(); break; default: $("#btnClusterResync").show(); $("#btnClusterOptions").show(); $("#btnClusterLeave").show(); $("#btnClusterDelete").hide(); break; } $("#tbodyAdminCluster").html(tableHtmlRows); $("#tfootAdminCluster").html("Total Nodes: " + responseJSON.response.clusterNodes.length); } else { $("#divAdminClusterInitialize").show(); $("#btnClusterResync").hide(); $("#btnClusterOptions").hide(); $("#btnClusterLeave").hide(); $("#btnClusterDelete").hide(); $("#tbodyAdminCluster").html("Cluster Not Initialized"); $("#tfootAdminCluster").html(""); } } function showEditSelfClusterNodeModal(objMenuItem) { var mnuItem = $(objMenuItem); var nodeName = mnuItem.attr("data-node-name"); var nodeIp = mnuItem.attr("data-node-ip"); var divEditClusterNodeAlert = $("#divEditClusterNodeAlert"); var divEditClusterNodeLoader = $("#divEditClusterNodeLoader"); var divEditClusterNodeView = $("#divEditClusterNodeView"); var node = $("#optAdminClusterNode").val(); $("#lblEditClusterNodeName").text(nodeName); $("#txtEditClusterNodeSelfNodeIpAddresses").val(nodeIp.replace(/,/g, "\n") + "\n"); $("#divEditClusterNodeSelfNode").show(); $("#divEditClusterNodePrimaryNode").hide(); $("#btnEditClusterNodeSave").attr("onclick", "updateSelfClusterNode(this); return false;"); divEditClusterNodeLoader.show(); divEditClusterNodeView.hide(); $("#modalEditClusterNode").modal("show"); HTTPRequest({ url: "api/admin/cluster/state?token=" + sessionData.token + "&includeServerIpAddresses=true" + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var optionsHtml = ""; for (var i = 0; i < responseJSON.response.serverIpAddresses.length; i++) optionsHtml += ""; $("#optEditClusterNodeQuickSelfIpAddresses").html(optionsHtml); divEditClusterNodeLoader.hide(); divEditClusterNodeView.show(); setTimeout(function () { $("#optEditClusterNodeSelfNodeIpAddress").trigger("focus"); }, 1000); }, error: function () { divEditClusterNodeLoader.hide(); }, invalidToken: function () { $("#modalEditClusterNode").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditClusterNodeAlert, objLoaderPlaceholder: divEditClusterNodeLoader }); } function updateSelfClusterNode(objBtn) { var divEditClusterNodeAlert = $("#divEditClusterNodeAlert"); var ipAddresses = cleanTextList($("#txtEditClusterNodeSelfNodeIpAddresses").val()); if ((ipAddresses.length === 0) || (ipAddresses === ",")) { showAlert("warning", "Missing!", "Please enter a node IP address.", divEditClusterNodeAlert); $("#txtEditClusterNodeSelfNodeIpAddresses").trigger("focus"); return; } var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/updateIpAddress?token=" + sessionData.token + "&ipAddresses=" + encodeURIComponent(ipAddresses) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalEditClusterNode").modal("hide"); reloadAdminClusterView(responseJSON); showAlert("success", "Node Updated!", "Cluster node was updated successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalEditClusterNode").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditClusterNodeAlert }); } function showEditPrimaryClusterNodeModal(objMenuItem) { var mnuItem = $(objMenuItem); var nodeName = mnuItem.attr("data-node-name"); var nodeUrl = mnuItem.attr("data-node-url"); var nodeIp = mnuItem.attr("data-node-ip"); $("#lblEditClusterNodeName").text(nodeName); $("#divEditClusterNodeSelfNode").hide(); $("#txtEditClusterNodePrimaryNodeUrl").val(nodeUrl); $("#txtEditClusterNodePrimaryNodeIpAddresses").val(nodeIp.replace(/,/g, "\n") + "\n"); $("#divEditClusterNodePrimaryNode").show(); $("#btnEditClusterNodeSave").attr("onclick", "updatePrimaryClusterNode(this); return false;"); hideAlert($("#divEditClusterNodeAlert")); $("#divEditClusterNodeLoader").hide(); $("#divEditClusterNodeView").show(); $("#modalEditClusterNode").modal("show"); setTimeout(function () { $("#txtEditClusterNodePrimaryNodeUrl").trigger("focus"); }, 1000); } function updatePrimaryClusterNode(objBtn) { var divEditClusterNodeAlert = $("#divEditClusterNodeAlert"); var primaryNodeUrl = $("#txtEditClusterNodePrimaryNodeUrl").val(); if (primaryNodeUrl === "") { showAlert("warning", "Missing!", "Please enter the Primary node URL.", divEditClusterNodeAlert); $("#txtEditClusterNodePrimaryNodeUrl").trigger("focus"); return; } var primaryNodeIpAddresses = cleanTextList($("#txtEditClusterNodePrimaryNodeIpAddresses").val()); if (primaryNodeIpAddresses === ",") primaryNodeIpAddresses = ""; var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/secondary/updatePrimary?token=" + sessionData.token + "&primaryNodeUrl=" + encodeURIComponent(primaryNodeUrl) + "&primaryNodeIpAddresses=" + encodeURIComponent(primaryNodeIpAddresses) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalEditClusterNode").modal("hide"); reloadAdminClusterView(responseJSON); showAlert("success", "Node Updated!", "Cluster node was updated successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalEditClusterNode").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditClusterNodeAlert }); } function showRemoveSecondaryClusterNodeModal(objMenuItem) { var mnuItem = $(objMenuItem); var secondaryNodeId = mnuItem.attr("data-node-id"); var nodeName = mnuItem.attr("data-node-name"); hideAlert($("#divRemoveClusterNodeAlert")); $("#lblRemoveClusterNodeName").text(nodeName); $("#chkRemoveClusterNodeForceRemove").prop("checked", false); $("#btnRemoveClusterNode").attr("data-node-id", secondaryNodeId); $("#modalRemoveClusterNode").modal("show"); } function removeSecondaryClusterNode(objBtn) { var divRemoveClusterNodeAlert = $("#divRemoveClusterNodeAlert"); var btn = $(objBtn); var secondaryNodeId = btn.attr("data-node-id"); var forceRemove = $("#chkRemoveClusterNodeForceRemove").prop("checked"); var apiUrl; if (forceRemove) apiUrl = "api/admin/cluster/primary/deleteSecondary"; else apiUrl = "api/admin/cluster/primary/removeSecondary"; var node = $("#optAdminClusterNode").val(); btn.button("loading"); HTTPRequest({ url: apiUrl + "?token=" + sessionData.token + "&secondaryNodeId=" + secondaryNodeId + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalRemoveClusterNode").modal("hide"); reloadAdminClusterView(responseJSON); showAlert("success", "Node Removed!", "Cluster node was removed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalRemoveClusterNode").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divRemoveClusterNodeAlert }); } function showPromoteToPrimaryClusterNodeModal(objMenuItem) { var mnuItem = $(objMenuItem); var nodeName = mnuItem.attr("data-node-name"); $("#lblPromoteToPrimaryClusterNodeName").text(nodeName); hideAlert($("#divPromoteToPrimaryClusterNodeAlert")); $("#chkPromoteToPrimaryClusterNodeForceDeletePrimary").prop("checked", false); $("#modalPromoteToPrimaryClusterNode").modal("show"); } function promoteToPrimaryClusterNode(objBtn) { var divPromoteToPrimaryClusterNodeAlert = $("#divPromoteToPrimaryClusterNodeAlert"); var forceDeletePrimary = $("#chkPromoteToPrimaryClusterNodeForceDeletePrimary").prop("checked"); var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/secondary/promote?token=" + sessionData.token + "&forceDeletePrimary=" + forceDeletePrimary + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#modalPromoteToPrimaryClusterNode").modal("hide"); btn.button("reset"); reloadAdminClusterView(responseJSON); showAlert("success", "Promoted!", "The selected node was successfully promoted to Primary node in the Cluster."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalPromoteToPrimaryClusterNode").modal("hide"); btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divPromoteToPrimaryClusterNodeAlert }); } function showInitializeClusterModal() { var divInitializeNewClusterAlert = $("#divInitializeNewClusterAlert"); var divInitializeNewClusterLoader = $("#divInitializeNewClusterLoader"); var divInitializeNewClusterView = $("#divInitializeNewClusterView"); divInitializeNewClusterLoader.show(); divInitializeNewClusterView.hide(); $("#modalInitializeNewCluster").modal("show"); HTTPRequest({ url: "api/admin/cluster/state?token=" + sessionData.token + "&includeServerIpAddresses=true", success: function (responseJSON) { if (responseJSON.response.clusterInitialized) { showAlert("danger", "Error!", "Cluster is already initialized.", divInitializeNewClusterAlert); return; } $("#txtInitializeNewClusterDomain").val(""); $("#txtInitializeNewClusterPrimaryNodeIpAddresses").val(""); var optionsHtml = ""; for (var i = 0; i < responseJSON.response.serverIpAddresses.length; i++) optionsHtml += ""; $("#optInitializeNewClusterQuickIpAddresses").html(optionsHtml); divInitializeNewClusterLoader.hide(); divInitializeNewClusterView.show(); setTimeout(function () { $("#txtInitializeNewClusterDomain").trigger("focus"); }, 1000); }, invalidToken: function () { $("#modalInitializeNewCluster").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divInitializeNewClusterAlert, objLoaderPlaceholder: divInitializeNewClusterLoader }); } function initializeNewCluster(objBtn) { var divInitializeNewClusterAlert = $("#divInitializeNewClusterAlert"); var clusterDomain = $("#txtInitializeNewClusterDomain").val(); if (clusterDomain === "") { showAlert("warning", "Missing!", "Please enter the Cluster domain name.", divInitializeNewClusterAlert); $("#txtInitializeNewClusterDomain").trigger("focus"); return; } var primaryNodeIpAddresses = cleanTextList($("#txtInitializeNewClusterPrimaryNodeIpAddresses").val()); if ((primaryNodeIpAddresses.length === 0) || (primaryNodeIpAddresses === ",")) { showAlert("warning", "Missing!", "Please enter a Primary node IP address.", divInitializeNewClusterAlert); $("#txtInitializeNewClusterPrimaryNodeIpAddresses").trigger("focus"); return; } var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/init?token=" + sessionData.token + "&clusterDomain=" + encodeURIComponent(clusterDomain) + "&primaryNodeIpAddresses=" + encodeURIComponent(primaryNodeIpAddresses), success: function (responseJSON) { $("#modalInitializeNewCluster").modal("hide"); btn.button("reset"); updateAdminClusterDataAndGui(responseJSON); reloadAdminClusterView(responseJSON); showAlert("success", "Cluster Initialized!", "A new cluster was initialized successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalInitializeNewCluster").modal("hide"); btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divInitializeNewClusterAlert }); } function showInitializeJoinClusterModal() { var divInitializeJoinClusterAlert = $("#divInitializeJoinClusterAlert"); var divInitializeJoinClusterLoader = $("#divInitializeJoinClusterLoader"); var divInitializeJoinClusterView = $("#divInitializeJoinClusterView"); divInitializeJoinClusterAlert.html(""); divInitializeJoinClusterLoader.show(); divInitializeJoinClusterView.hide(); $("#modalInitializeJoinCluster").modal("show"); HTTPRequest({ url: "api/admin/cluster/state?token=" + sessionData.token + "&includeServerIpAddresses=true", success: function (responseJSON) { if (responseJSON.response.clusterInitialized) { showAlert("danger", "Error!", "Cluster is already initialized.", divInitializeJoinClusterAlert); return; } $("#txtInitializeJoinClusterSecondaryNodeIpAddresses").val(""); var optionsHtml = ""; for (var i = 0; i < responseJSON.response.serverIpAddresses.length; i++) optionsHtml += ""; $("#optInitializeJoinClusterQuickIpAddresses").html(optionsHtml); $("#txtInitializeJoinClusterPrimaryNodeUrl").val(""); $("#txtInitializeJoinClusterPrimaryNodeIpAddress").val(""); $("#rdInitializeJoinClusterCertificateValidationDefault").prop("checked", true); $("#txtInitializeJoinClusterPrimaryNodeUsername").val("admin"); $("#txtInitializeJoinClusterPrimaryNodePassword").prop("disabled", false); $("#txtInitializeJoinClusterPrimaryNodePassword").val(""); $("#divInitializeJoinClusterPrimaryNode2faTotp").hide(); $("#txtInitializeJoinClusterPrimaryNode2faTotp").val(""); divInitializeJoinClusterLoader.hide(); divInitializeJoinClusterView.show(); setTimeout(function () { $("#txtInitializeJoinClusterSecondaryNodeIpAddresses").trigger("focus"); }, 1000); }, invalidToken: function () { $("#modalInitializeJoinCluster").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divInitializeJoinClusterAlert, objLoaderPlaceholder: divInitializeJoinClusterLoader }); } function initializeJoinCluster(objBtn) { var divInitializeJoinClusterAlert = $("#divInitializeJoinClusterAlert"); var secondaryNodeIpAddresses = cleanTextList($("#txtInitializeJoinClusterSecondaryNodeIpAddresses").val()); if ((secondaryNodeIpAddresses.length === 0) || (secondaryNodeIpAddresses === ",")) { showAlert("warning", "Missing!", "Please select a Secondary node IP address.", divInitializeJoinClusterAlert); $("#txtInitializeJoinClusterSecondaryNodeIpAddresses").trigger("focus"); return; } var primaryNodeUrl = $("#txtInitializeJoinClusterPrimaryNodeUrl").val(); if (primaryNodeUrl === "") { showAlert("warning", "Missing!", "Please enter the Primary node URL.", divInitializeJoinClusterAlert); $("#txtInitializeJoinClusterPrimaryNodeUrl").trigger("focus"); return; } var primaryNodeIpAddress = $("#txtInitializeJoinClusterPrimaryNodeIpAddress").val(); var ignoreCertificateErrors = $("input[name=rdInitializeJoinClusterCertificateValidation]:checked").val(); var primaryNodeUsername = $("#txtInitializeJoinClusterPrimaryNodeUsername").val(); if (primaryNodeUsername === "") { showAlert("warning", "Missing!", "Please enter the Primary node admin username.", divInitializeJoinClusterAlert); $("#txtInitializeJoinClusterPrimaryNodeUsername").trigger("focus"); return; } var primaryNodePassword = $("#txtInitializeJoinClusterPrimaryNodePassword").val(); if (primaryNodePassword === "") { showAlert("warning", "Missing!", "Please enter the Primary node admin password.", divInitializeJoinClusterAlert); $("#txtInitializeJoinClusterPrimaryNodePassword").trigger("focus"); return; } var primaryNodeTotp = $("#txtInitializeJoinClusterPrimaryNode2faTotp").val(); if ($("#divInitializeJoinClusterPrimaryNode2faTotp").is(":visible")) { if (primaryNodeTotp === "") { showAlert("warning", "Missing!", "Please enter the Primary node admin user's OTP.", divInitializeJoinClusterAlert); $("#txtInitializeJoinClusterPrimaryNode2faTotp").trigger("focus"); return; } } var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/initJoin", method: "POST", data: "token=" + sessionData.token + "&secondaryNodeIpAddresses=" + encodeURIComponent(secondaryNodeIpAddresses) + "&primaryNodeUrl=" + encodeURIComponent(primaryNodeUrl) + "&primaryNodeIpAddress=" + encodeURIComponent(primaryNodeIpAddress) + "&ignoreCertificateErrors=" + ignoreCertificateErrors + "&primaryNodeUsername=" + encodeURIComponent(primaryNodeUsername) + "&primaryNodePassword=" + encodeURIComponent(primaryNodePassword) + "&primaryNodeTotp=" + encodeURIComponent(primaryNodeTotp), processData: false, success: function (responseJSON) { $("#modalInitializeJoinCluster").modal("hide"); btn.button("reset"); updateAdminClusterDataAndGui(responseJSON); reloadAdminClusterView(responseJSON); showAlert("success", "Joined Cluster!", "Joined the cluster successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalInitializeJoinCluster").modal("hide"); btn.button("reset"); showPageLogin(); }, twoFactorAuthRequired: function () { btn.button("reset"); $("#txtInitializeJoinClusterPrimaryNodePassword").prop("disabled", true); $("#divInitializeJoinClusterPrimaryNode2faTotp").show(); $("#txtInitializeJoinClusterPrimaryNode2faTotp").trigger("focus"); }, objAlertPlaceholder: divInitializeJoinClusterAlert }); } function resyncCluster(objBtn) { if (!confirm("The resync Cluster action will initiate a full config transfer from the Primary node. You will need to check the logs to confirm if the resync action was successful.\r\n\r\nAre you sure you want to resync the Cluster config?")) return; var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/secondary/resync?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); showAlert("success", "Resync Triggered!", "A full config resync was triggered successfully. Please check the Logs for confirmation."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function showClusterOptionsModal() { var divClusterOptionsAlert = $("#divClusterOptionsAlert"); var divClusterOptionsLoader = $("#divClusterOptionsLoader"); var divClusterOptionsView = $("#divClusterOptionsView"); divClusterOptionsLoader.show(); divClusterOptionsView.hide(); var node = $("#optAdminClusterNode").val(); $("#modalClusterOptions").modal("show"); HTTPRequest({ url: "api/admin/cluster/state?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var selfNodeType; for (var i = 0; i < responseJSON.response.clusterNodes.length; i++) { if (responseJSON.response.clusterNodes[i].state == "Self") { selfNodeType = responseJSON.response.clusterNodes[i].type; break; } } var isPrimaryNode = selfNodeType == "Primary"; $("#txtClusterOptionsHeartbeatRefreshIntervalSeconds").attr("disabled", !isPrimaryNode); $("#txtClusterOptionsHeartbeatRetryIntervalSeconds").attr("disabled", !isPrimaryNode); $("#txtClusterOptionsConfigRefreshIntervalSeconds").attr("disabled", !isPrimaryNode); $("#txtClusterOptionsConfigRetryIntervalSeconds").attr("disabled", !isPrimaryNode); if (isPrimaryNode) $("#btnClusterOptionsSave").show(); else $("#btnClusterOptionsSave").hide(); $("#txtClusterOptionsClusterDomain").val(responseJSON.response.clusterDomain); $("#txtClusterOptionsHeartbeatRefreshIntervalSeconds").val(responseJSON.response.heartbeatRefreshIntervalSeconds); $("#txtClusterOptionsHeartbeatRetryIntervalSeconds").val(responseJSON.response.heartbeatRetryIntervalSeconds); $("#txtClusterOptionsConfigRefreshIntervalSeconds").val(responseJSON.response.configRefreshIntervalSeconds); $("#txtClusterOptionsConfigRetryIntervalSeconds").val(responseJSON.response.configRetryIntervalSeconds); divClusterOptionsLoader.hide(); divClusterOptionsView.show(); setTimeout(function () { $("#txtClusterOptionsHeartbeatRefreshIntervalSeconds").trigger("focus"); }, 1000); }, invalidToken: function () { $("#modalClusterOptions").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divClusterOptionsAlert, objLoaderPlaceholder: divClusterOptionsLoader }); } function saveClusterOptions(objBtn) { var divClusterOptionsAlert = $("#divClusterOptionsAlert"); var heartbeatRefreshIntervalSeconds = $("#txtClusterOptionsHeartbeatRefreshIntervalSeconds").val(); if (heartbeatRefreshIntervalSeconds === "") { showAlert("warning", "Missing!", "Please enter a value for Heartbeat Refresh Interval.", divClusterOptionsAlert); $("#txtClusterOptionsHeartbeatRefreshIntervalSeconds").trigger("focus"); return; } var heartbeatRetryIntervalSeconds = $("#txtClusterOptionsHeartbeatRetryIntervalSeconds").val(); if (heartbeatRetryIntervalSeconds === "") { showAlert("warning", "Missing!", "Please enter a value for Heartbeat Retry Interval.", divClusterOptionsAlert); $("#txtClusterOptionsHeartbeatRetryIntervalSeconds").trigger("focus"); return; } var configRefreshIntervalSeconds = $("#txtClusterOptionsConfigRefreshIntervalSeconds").val(); if (configRefreshIntervalSeconds === "") { showAlert("warning", "Missing!", "Please enter a value for Config Refresh Interval.", divClusterOptionsAlert); $("#txtClusterOptionsConfigRefreshIntervalSeconds").trigger("focus"); return; } var configRetryIntervalSeconds = $("#txtClusterOptionsConfigRetryIntervalSeconds").val(); if (configRetryIntervalSeconds === "") { showAlert("warning", "Missing!", "Please enter a value for Config Retry Interval.", divClusterOptionsAlert); $("#txtClusterOptionsConfigRetryIntervalSeconds").trigger("focus"); return; } var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/primary/setOptions?token=" + sessionData.token + "&heartbeatRefreshIntervalSeconds=" + heartbeatRefreshIntervalSeconds + "&heartbeatRetryIntervalSeconds=" + heartbeatRetryIntervalSeconds + "&configRefreshIntervalSeconds=" + configRefreshIntervalSeconds + "&configRetryIntervalSeconds=" + configRetryIntervalSeconds + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#modalClusterOptions").modal("hide"); btn.button("reset"); showAlert("success", "Options Saved!", "The Cluster options were saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalClusterOptions").modal("hide"); btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divClusterOptionsAlert, }); } function showLeaveClusterModal() { hideAlert($("#divLeaveClusterAlert")); $("#chkLeaveClusterForceLeave").prop("checked", false); $("#modalLeaveCluster").modal("show"); } function leaveCluster(objBtn) { var divLeaveClusterAlert = $("#divLeaveClusterAlert"); var forceLeave = $("#chkLeaveClusterForceLeave").prop("checked"); var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/secondary/leave?token=" + sessionData.token + "&forceLeave=" + forceLeave + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#modalLeaveCluster").modal("hide"); btn.button("reset"); updateAdminClusterDataAndGui(responseJSON); reloadAdminClusterView(responseJSON); showAlert("success", "Left Cluster!", "Left the Cluster successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalLeaveCluster").modal("hide"); btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divLeaveClusterAlert }); } function showDeleteClusterModal() { hideAlert($("#divDeleteClusterAlert")); $("#chkDeleteClusterForceDelete").prop("checked", false); $("#modalDeleteCluster").modal("show"); } function deleteCluster(objBtn) { var divDeleteClusterAlert = $("#divDeleteClusterAlert"); var forceDelete = $("#chkDeleteClusterForceDelete").prop("checked"); var node = $("#optAdminClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/admin/cluster/primary/delete?token=" + sessionData.token + "&forceDelete=" + forceDelete + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#modalDeleteCluster").modal("hide"); btn.button("reset"); updateAdminClusterDataAndGui(responseJSON); reloadAdminClusterView(responseJSON); showAlert("success", "Cluster Deleted!", "Cluster was deleted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalDeleteCluster").modal("hide"); btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divDeleteClusterAlert }); } function getPrimaryClusterNodeName() { if (sessionData.info.clusterInitialized) { for (var i = 0; i < sessionData.info.clusterNodes.length; i++) { if (sessionData.info.clusterNodes[i].type == "Primary") return sessionData.info.clusterNodes[i].name; } } return ""; } function updateAllClusterNodeDropDowns() { updateClusterNodeDropDown($("#optDashboardClusterNode"), true); updateClusterNodeDropDown($("#optZonesClusterNode")); updateClusterNodeDropDown($("#optEditZoneClusterNode")); updateClusterNodeDropDown($("#optCachedZonesClusterNode")); updateClusterNodeDropDown($("#optDnsClientClusterNode")); updateClusterNodeDropDown($("#optSettingsClusterNode"), true); updateClusterNodeDropDown($("#optDhcpClusterNode")); updateClusterNodeDropDown($("#optAdminSessionsClusterNode")); updateClusterNodeDropDown($("#optAdminClusterNode")); updateClusterNodeDropDown($("#optLogsClusterNode")); } function updateClusterNodeDropDown(optClusterNode, addClusterNode, selectedNode) { if (sessionData.info.clusterInitialized) { if (selectedNode == null) selectedNode = optClusterNode.val(); var html = ""; if (addClusterNode) html += ""; for (var i = 0; i < sessionData.info.clusterNodes.length; i++) html += ""; optClusterNode.html(html); if ((selectedNode == null) || (selectedNode == "")) { if (addClusterNode) selectedNode = "cluster"; else selectedNode = sessionData.info.dnsServerDomain; } optClusterNode.val(selectedNode); if ((optClusterNode.val() == null) && (sessionData.info.clusterNodes.length > 0)) optClusterNode.val(sessionData.info.clusterNodes[0].name); optClusterNode.show(); } else { optClusterNode.hide(); optClusterNode.html(""); optClusterNode.val(""); } } ================================================ FILE: DnsServerCore/www/js/common.js ================================================ /* 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 . */ function htmlEncode(value) { return $('
').text(value).html().replace(/"/g, """); } function htmlDecode(value) { return $('
').html(value).text(); } function HTTPRequest(url, method, data, isTextResponse, success, error, invalidToken, twoFactorAuthRequired, objAlertPlaceholder, objLoaderPlaceholder, processData, contentType, dontHideAlert, showInnerError) { var finalUrl; if ((url != null) && (url.url != null)) finalUrl = arguments[0].url; else finalUrl = url; if (method == null) method = arguments[0].method; if (method == null) method = "GET"; if (data == null) { if (arguments[0].data == null) data = ""; else data = arguments[0].data; } if (isTextResponse == null) isTextResponse = arguments[0].isTextResponse; if (isTextResponse == null) isTextResponse = false; var dataType = isTextResponse ? null : "json"; if (success == null) success = arguments[0].success; var async = success != null; if (error == null) error = arguments[0].error; if (invalidToken == null) invalidToken = arguments[0].invalidToken; if (twoFactorAuthRequired == null) twoFactorAuthRequired = arguments[0].twoFactorAuthRequired; if (objAlertPlaceholder == null) objAlertPlaceholder = arguments[0].objAlertPlaceholder; if (dontHideAlert == null) dontHideAlert = arguments[0].dontHideAlert; if ((dontHideAlert == null) || !dontHideAlert) hideAlert(objAlertPlaceholder); if (showInnerError == null) showInnerError = arguments[0].showInnerError; if (showInnerError == null) showInnerError = false; if (objLoaderPlaceholder == null) objLoaderPlaceholder = arguments[0].objLoaderPlaceholder; if (processData == null) processData = arguments[0].processData; if (contentType == null) contentType = arguments[0].contentType; if (objLoaderPlaceholder != null) objLoaderPlaceholder.html("
"); var successFlag = false; $.ajax({ type: method, url: finalUrl, data: data, dataType: dataType, async: async, cache: false, processData: processData, contentType: contentType, success: function (response, status, jqXHR) { if (objLoaderPlaceholder != null) objLoaderPlaceholder.html(""); if (isTextResponse) { if (success == null) successFlag = true; else success(response); } else { switch (response.status) { case "ok": if (success == null) successFlag = true; else success(response); break; case "invalid-token": if (invalidToken != null) invalidToken(); else { showAlert("danger", "Error!", response.errorMessage + (showInnerError && (response.innerErrorMessage != null) ? " " + response.innerErrorMessage : ""), objAlertPlaceholder); if (error != null) error(); else window.location = "/"; } break; case "2fa-required": if (twoFactorAuthRequired != null) { twoFactorAuthRequired(); } else { showAlert("danger", "Error!", response.errorMessage + (showInnerError && (response.innerErrorMessage != null) ? " " + response.innerErrorMessage : ""), objAlertPlaceholder); if (error != null) error(); } break; case "error": showAlert("danger", "Error!", response.errorMessage + (showInnerError && (response.innerErrorMessage != null) ? " " + response.innerErrorMessage : ""), objAlertPlaceholder); if (error != null) error(); break; default: showAlert("danger", "Invalid Response!", "Server returned invalid response status: " + response.status, objAlertPlaceholder); if (error != null) error(); break; } } }, error: function (jqXHR, textStatus, errorThrown) { if (objLoaderPlaceholder != null) objLoaderPlaceholder.html(""); if (error != null) error(); var msg; if ((textStatus === "error") && (errorThrown === "")) msg = "Unable to connect to the server. Please try again." else msg = textStatus + " - " + errorThrown; showAlert("danger", "Error!", msg, objAlertPlaceholder); } }); return successFlag; } function showAlert(type, title, message, objAlertPlaceholder) { var alertHTML = "
\ \ " + title + " " + htmlEncode(message) + "\
"; if (objAlertPlaceholder == null) objAlertPlaceholder = $(".AlertPlaceholder"); objAlertPlaceholder.html(alertHTML); if (type == "success") { setTimeout(function () { hideAlert(objAlertPlaceholder); }, 5000); } } function hideAlert(objAlertPlaceholder) { if (objAlertPlaceholder == null) objAlertPlaceholder = $(".AlertPlaceholder"); objAlertPlaceholder.html(""); } function sortTable(tableId, n) { var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0; table = document.getElementById(tableId); switching = true; // Set the sorting direction to ascending: dir = "asc"; /* Make a loop that will continue until no switching has been done: */ while (switching) { // Start by saying: no switching is done: switching = false; rows = table.rows; /* Loop through all table rows */ for (i = 0; i < (rows.length - 1); i++) { // Start by saying there should be no switching: shouldSwitch = false; /* Get the two elements you want to compare, one from current row and one from the next: */ x = rows[i].getElementsByTagName("TD")[n]; y = rows[i + 1].getElementsByTagName("TD")[n]; /* Check if the two rows should switch place, based on the direction, asc or desc: */ if (dir == "asc") { if (x.innerText.toLowerCase() > y.innerText.toLowerCase()) { // If so, mark as a switch and break the loop: shouldSwitch = true; break; } } else if (dir == "desc") { if (x.innerText.toLowerCase() < y.innerText.toLowerCase()) { // If so, mark as a switch and break the loop: shouldSwitch = true; break; } } } if (shouldSwitch) { /* If a switch has been marked, make the switch and mark that a switch has been done: */ rows[i].parentNode.insertBefore(rows[i + 1], rows[i]); switching = true; // Each time a switch is done, increase this count by 1: switchcount++; } else { /* If no switching has been done AND the direction is "asc", set the direction to "desc" and run the while loop again. */ if (switchcount == 0 && dir == "asc") { dir = "desc"; switching = true; } } } } function serializeTableData(table, columns, objAlertPlaceholder) { var data = table.find('input:text, :input[type="number"], input:checkbox, input:hidden, select'); var output = ""; for (var i = 0; i < data.length; i += columns) { if (i > 0) output += "|"; for (var j = 0; j < columns; j++) { if (j > 0) output += "|"; var cell = $(data[i + j]); var cellValue; if (cell.attr("type") == "checkbox") { cellValue = cell.prop("checked").toString(); } else { cellValue = cell.val(); var optional = (cell.attr("data-optional") === "true"); if ((cellValue === "") && !optional) { showAlert("warning", "Missing!", "Please enter a valid value in the text field in focus.", objAlertPlaceholder); cell.focus(); return false; } if (cellValue.includes("|")) { showAlert("warning", "Invalid Character!", "Please edit the value in the text field in focus to remove '|' character.", objAlertPlaceholder); cell.focus(); return false; } } output += htmlDecode(cellValue); } } return output; } function cleanTextList(text) { text = text.replace(/\n/g, ","); while (text.indexOf(",,") !== -1) { text = text.replace(/,,/g, ","); } if (text.startsWith(",")) text = text.substr(1); if (text.endsWith(",")) text = text.substr(0, text.length - 1); return text; } ================================================ FILE: DnsServerCore/www/js/dhcp.js ================================================ /* 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 . */ $(function () { $("#chkDhcpScopeDnsUpdates").on("click", function () { var checked = $("#chkDhcpScopeDnsUpdates").prop("checked"); $("#chkDnsOverwriteForDynamicLease").prop("disabled", !checked); }); }); function refreshDhcpTab() { if ($("#dhcpTabListLeases").hasClass("active")) refreshDhcpLeases(); else if ($("#dhcpTabListScopes").hasClass("active")) refreshDhcpScopes(true); else refreshDhcpLeases(); } function refreshDhcpLeases() { var node = $("#optDhcpClusterNode").val(); var divDhcpLeasesLoader = $("#divDhcpLeasesLoader"); var divDhcpLeases = $("#divDhcpLeases"); divDhcpLeases.hide(); divDhcpLeasesLoader.show(); HTTPRequest({ url: "api/dhcp/leases/list?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var dhcpLeases = responseJSON.response.leases; var tableHtmlRows = ""; for (var i = 0; i < dhcpLeases.length; i++) { tableHtmlRows += "" + htmlEncode(dhcpLeases[i].scope) + "" + dhcpLeases[i].hardwareAddress + "" + dhcpLeases[i].address + "" + dhcpLeases[i].type + "" + htmlEncode(dhcpLeases[i].hostName) + "" + moment(dhcpLeases[i].leaseObtained).local().format("YYYY-MM-DD HH:mm") + "" + moment(dhcpLeases[i].leaseExpires).local().format("YYYY-MM-DD HH:mm"); tableHtmlRows += "
"; } $("#tableDhcpLeasesBody").html(tableHtmlRows); if (dhcpLeases.length > 0) $("#tableDhcpLeasesFooter").html("Total Leases: " + dhcpLeases.length + ""); else $("#tableDhcpLeasesFooter").html("No Lease Found"); divDhcpLeasesLoader.hide(); divDhcpLeases.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDhcpLeasesLoader }); } function convertToReservedLease(id, scopeName, clientIdentifier) { if (!confirm("Are you sure you want to convert the dynamic lease to reserved lease?")) return; var node = $("#optDhcpClusterNode").val(); var btn = $("#btnDhcpLeaseRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/dhcp/leases/convertToReserved?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&clientIdentifier=" + encodeURIComponent(clientIdentifier) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); $("#btnDhcpLeaseReserve" + id).hide(); $("#btnDhcpLeaseUnreserve" + id).show(); var spanDhcpLeaseType = $("#spanDhcpLeaseType" + id); spanDhcpLeaseType.html("Reserved"); spanDhcpLeaseType.attr("class", "label label-default"); showAlert("success", "Reserved!", "The dynamic lease was converted to reserved lease successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function convertToDynamicLease(id, scopeName, clientIdentifier) { if (!confirm("Are you sure you want to convert the reserved lease to dynamic lease?")) return; var node = $("#optDhcpClusterNode").val(); var btn = $("#btnDhcpLeaseRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/dhcp/leases/convertToDynamic?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&clientIdentifier=" + encodeURIComponent(clientIdentifier) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); $("#btnDhcpLeaseReserve" + id).show(); $("#btnDhcpLeaseUnreserve" + id).hide(); var spanDhcpLeaseType = $("#spanDhcpLeaseType" + id); spanDhcpLeaseType.html("Dynamic"); spanDhcpLeaseType.attr("class", "label label-primary"); showAlert("success", "Unreserved!", "The reserved lease was converted to dynamic lease successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function showRemoveLeaseModal(index, scopeName, clientIdentifier) { $("#divDhcpRemoveLeaseAlert").html(""); $("#btnRemoveDhcpLease").attr("onclick", "removeLease(this, " + index + ", '" + scopeName + "', '" + clientIdentifier + "');"); $("#modalDhcpRemoveLease").modal("show"); } function removeLease(objBtn, index, scopeName, clientIdentifier) { var divDhcpRemoveLeaseAlert = $("#divDhcpRemoveLeaseAlert"); var node = $("#optDhcpClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/dhcp/leases/remove?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&clientIdentifier=" + encodeURIComponent(clientIdentifier) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalDhcpRemoveLease").modal("hide"); $("#trDhcpLeaseRow" + index).remove(); var dhcpLeasesLength = $('#tableDhcpLeasesBody >tr').length; if (dhcpLeasesLength > 0) $("#tableDhcpLeasesFooter").html("Total Leases: " + dhcpLeasesLength + ""); else $("#tableDhcpLeasesFooter").html("No Lease Found"); showAlert("success", "Lease Removed!", "The DHCP lease was removed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { showPageLogin(); }, objAlertPlaceholder: divDhcpRemoveLeaseAlert }); } function refreshDhcpScopes(checkDisplay) { if (checkDisplay == null) checkDisplay = false; var divDhcpEditScope = $("#divDhcpEditScope"); if (checkDisplay && (divDhcpEditScope.css("display") != "none")) return; var node = $("#optDhcpClusterNode").val(); $("#optDhcpClusterNode").prop("disabled", false); var divDhcpViewScopes = $("#divDhcpViewScopes"); var divDhcpViewScopesLoader = $("#divDhcpViewScopesLoader"); divDhcpViewScopes.hide(); divDhcpEditScope.hide(); divDhcpViewScopesLoader.show(); HTTPRequest({ url: "api/dhcp/scopes/list?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var dhcpScopes = responseJSON.response.scopes; var tableHtmlRows = ""; for (var i = 0; i < dhcpScopes.length; i++) { tableHtmlRows += "" + htmlEncode(dhcpScopes[i].name) + "" + dhcpScopes[i].startingAddress + " - " + dhcpScopes[i].endingAddress + "
" + dhcpScopes[i].subnetMask + "" + dhcpScopes[i].networkAddress + "
" + dhcpScopes[i].broadcastAddress + "" + (dhcpScopes[i].interfaceAddress == null ? "" : dhcpScopes[i].interfaceAddress) + ""; tableHtmlRows += ""; if (dhcpScopes[i].enabled) tableHtmlRows += ""; else tableHtmlRows += ""; tableHtmlRows += ""; } $("#tableDhcpScopesBody").html(tableHtmlRows); if (dhcpScopes.length > 0) $("#tableDhcpScopesFooter").html("Total Scopes: " + dhcpScopes.length + ""); else $("#tableDhcpScopesFooter").html("No Scope Found"); divDhcpViewScopesLoader.hide(); divDhcpViewScopes.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDhcpViewScopesLoader }); } function addDhcpScopeStaticRouteRow(destination, subnetMask, router) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableDhcpScopeStaticRoutes").append(tableHtmlRows); } function addDhcpScopeVendorInfoRow(identifier, information) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableDhcpScopeVendorInfo").append(tableHtmlRows); } function addDhcpScopeGenericOptionsRow(optionCode, hexValue) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableDhcpScopeGenericOptions").append(tableHtmlRows); } function addDhcpScopeExclusionRow(startingAddress, endingAddress) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableDhcpScopeExclusions").append(tableHtmlRows); } function addDhcpScopeReservedLeaseRow(hostName, hardwareAddress, address, comments) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableDhcpScopeReservedLeases").append(tableHtmlRows); } function clearDhcpScopeForm() { $("#txtDhcpScopeName").attr("data-name", ""); $("#txtDhcpScopeName").val(""); $("#txtDhcpScopeStartingAddress").val(""); $("#txtDhcpScopeEndingAddress").val(""); $("#txtDhcpScopeSubnetMask").val(""); $("#txtDhcpScopeLeaseTimeDays").val("1"); $("#txtDhcpScopeLeaseTimeHours").val("0"); $("#txtDhcpScopeLeaseTimeMinutes").val("0"); $("#txtDhcpScopeOfferDelayTime").val("0"); $("#chkDhcpScopePingCheckEnabled").prop("checked", false); $("#txtDhcpScopePingCheckTimeout").val("1000"); $("#txtDhcpScopePingCheckRetries").val("2"); $("#txtDhcpScopeDomainName").val(""); $("#txtDhcpScopeDomainSearchStrings").val(""); $("#chkDhcpScopeDnsUpdates").prop("checked", true); $("#chkDnsOverwriteForDynamicLease").prop("disabled", false); $("#chkDnsOverwriteForDynamicLease").prop("checked", false); $("#txtDhcpScopeDnsTtl").val("900"); $("#txtDhcpScopeServerAddress").val(""); $("#txtDhcpScopeServerHostName").val(""); $("#txtDhcpScopeBootFileName").val(""); $("#txtDhcpScopeRouterAddress").val(""); $("#chkUseThisDnsServer").prop("checked", false); $('#txtDhcpScopeDnsServers').prop("disabled", false); $("#txtDhcpScopeDnsServers").val(""); $("#txtDhcpScopeWinsServers").val(""); $("#txtDhcpScopeNtpServers").val(""); $("#txtDhcpScopeNtpServerDomainNames").val(""); $("#tableDhcpScopeStaticRoutes").html(""); $("#tableDhcpScopeVendorInfo").html(""); $("#txtDhcpScopeCAPWAPApIpAddresses").val(""); $("#txtDhcpScopeTftpServerAddresses").val(""); $("#tableDhcpScopeGenericOptions").html(""); $("#tableDhcpScopeExclusions").html(""); $("#tableDhcpScopeReservedLeases").html(""); $("#chkAllowOnlyReservedLeases").prop("checked", false); $("#chkBlockLocallyAdministeredMacAddresses").prop("checked", false); $("#chkIgnoreClientIdentifierOption").prop("checked", true); $("#btnSaveDhcpScope").button("reset"); } function showAddDhcpScope() { clearDhcpScopeForm(); $("#titleDhcpEditScope").html("Add Scope"); $("#chkUseThisDnsServer").prop("checked", true); $('#txtDhcpScopeDnsServers').prop("disabled", true); $("#divDhcpViewScopes").hide(); $("#divDhcpViewScopesLoader").hide(); $("#divDhcpEditScope").show(); } function showEditDhcpScope(scopeName) { clearDhcpScopeForm(); var node = $("#optDhcpClusterNode").val(); $("#titleDhcpEditScope").html("Edit Scope"); var divDhcpViewScopesLoader = $("#divDhcpViewScopesLoader"); var divDhcpViewScopes = $("#divDhcpViewScopes"); var divDhcpEditScope = $("#divDhcpEditScope"); divDhcpViewScopes.hide(); divDhcpEditScope.hide(); divDhcpViewScopesLoader.show(); HTTPRequest({ url: "api/dhcp/scopes/get?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#txtDhcpScopeName").attr("data-name", responseJSON.response.name); $("#txtDhcpScopeName").val(responseJSON.response.name); $("#txtDhcpScopeStartingAddress").val(responseJSON.response.startingAddress); $("#txtDhcpScopeEndingAddress").val(responseJSON.response.endingAddress); $("#txtDhcpScopeSubnetMask").val(responseJSON.response.subnetMask); $("#txtDhcpScopeLeaseTimeDays").val(responseJSON.response.leaseTimeDays); $("#txtDhcpScopeLeaseTimeHours").val(responseJSON.response.leaseTimeHours); $("#txtDhcpScopeLeaseTimeMinutes").val(responseJSON.response.leaseTimeMinutes); $("#txtDhcpScopeOfferDelayTime").val(responseJSON.response.offerDelayTime); $("#chkDhcpScopePingCheckEnabled").prop("checked", responseJSON.response.pingCheckEnabled); $("#txtDhcpScopePingCheckTimeout").val(responseJSON.response.pingCheckTimeout); $("#txtDhcpScopePingCheckRetries").val(responseJSON.response.pingCheckRetries); if (responseJSON.response.domainName != null) $("#txtDhcpScopeDomainName").val(responseJSON.response.domainName); if (responseJSON.response.domainSearchList != null) $("#txtDhcpScopeDomainSearchStrings").val(responseJSON.response.domainSearchList.join("\n")); $("#chkDhcpScopeDnsUpdates").prop("checked", responseJSON.response.dnsUpdates); $("#chkDnsOverwriteForDynamicLease").prop("disabled", !responseJSON.response.dnsUpdates); $("#chkDnsOverwriteForDynamicLease").prop("checked", responseJSON.response.dnsOverwriteForDynamicLease); $("#txtDhcpScopeDnsTtl").val(responseJSON.response.dnsTtl); if (responseJSON.response.serverAddress != null) $("#txtDhcpScopeServerAddress").val(responseJSON.response.serverAddress); if (responseJSON.response.serverHostName != null) $("#txtDhcpScopeServerHostName").val(responseJSON.response.serverHostName); if (responseJSON.response.bootFileName != null) $("#txtDhcpScopeBootFileName").val(responseJSON.response.bootFileName); if (responseJSON.response.routerAddress != null) $("#txtDhcpScopeRouterAddress").val(responseJSON.response.routerAddress); $("#chkUseThisDnsServer").prop("checked", responseJSON.response.useThisDnsServer); $('#txtDhcpScopeDnsServers').prop("disabled", responseJSON.response.useThisDnsServer); if (responseJSON.response.dnsServers != null) $("#txtDhcpScopeDnsServers").val(responseJSON.response.dnsServers.join("\n")); if (responseJSON.response.winsServers != null) $("#txtDhcpScopeWinsServers").val(responseJSON.response.winsServers.join("\n")); if (responseJSON.response.ntpServers != null) $("#txtDhcpScopeNtpServers").val(responseJSON.response.ntpServers.join("\n")); if (responseJSON.response.ntpServerDomainNames != null) $("#txtDhcpScopeNtpServerDomainNames").val(responseJSON.response.ntpServerDomainNames.join("\n")); if (responseJSON.response.staticRoutes != null) { for (var i = 0; i < responseJSON.response.staticRoutes.length; i++) { addDhcpScopeStaticRouteRow(responseJSON.response.staticRoutes[i].destination, responseJSON.response.staticRoutes[i].subnetMask, responseJSON.response.staticRoutes[i].router); } } if (responseJSON.response.vendorInfo != null) { for (var i = 0; i < responseJSON.response.vendorInfo.length; i++) { addDhcpScopeVendorInfoRow(responseJSON.response.vendorInfo[i].identifier, responseJSON.response.vendorInfo[i].information); } } if (responseJSON.response.capwapAcIpAddresses != null) $("#txtDhcpScopeCAPWAPApIpAddresses").val(responseJSON.response.capwapAcIpAddresses.join("\n")); if (responseJSON.response.tftpServerAddresses != null) $("#txtDhcpScopeTftpServerAddresses").val(responseJSON.response.tftpServerAddresses.join("\n")); if (responseJSON.response.genericOptions != null) { for (var i = 0; i < responseJSON.response.genericOptions.length; i++) { addDhcpScopeGenericOptionsRow(responseJSON.response.genericOptions[i].code, responseJSON.response.genericOptions[i].value); } } if (responseJSON.response.exclusions != null) { for (var i = 0; i < responseJSON.response.exclusions.length; i++) { addDhcpScopeExclusionRow(responseJSON.response.exclusions[i].startingAddress, responseJSON.response.exclusions[i].endingAddress); } } if (responseJSON.response.reservedLeases != null) { for (var i = 0; i < responseJSON.response.reservedLeases.length; i++) { addDhcpScopeReservedLeaseRow(responseJSON.response.reservedLeases[i].hostName, responseJSON.response.reservedLeases[i].hardwareAddress, responseJSON.response.reservedLeases[i].address, responseJSON.response.reservedLeases[i].comments); } } $("#chkAllowOnlyReservedLeases").prop("checked", responseJSON.response.allowOnlyReservedLeases); $("#chkBlockLocallyAdministeredMacAddresses").prop("checked", responseJSON.response.blockLocallyAdministeredMacAddresses); $("#chkIgnoreClientIdentifierOption").prop("checked", responseJSON.response.ignoreClientIdentifierOption); $("#optDhcpClusterNode").prop("disabled", true); divDhcpViewScopesLoader.hide(); divDhcpEditScope.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDhcpViewScopesLoader }); } function saveDhcpScope() { var oldName = $("#txtDhcpScopeName").attr("data-name"); var name = $("#txtDhcpScopeName").val(); var newName = null; if ((oldName !== "") && (oldName != name)) { newName = name; name = oldName; } var startingAddress = $("#txtDhcpScopeStartingAddress").val(); var endingAddress = $("#txtDhcpScopeEndingAddress").val(); var subnetMask = $("#txtDhcpScopeSubnetMask").val(); var leaseTimeDays = $("#txtDhcpScopeLeaseTimeDays").val(); var leaseTimeHours = $("#txtDhcpScopeLeaseTimeHours").val(); var leaseTimeMinutes = $("#txtDhcpScopeLeaseTimeMinutes").val(); var offerDelayTime = $("#txtDhcpScopeOfferDelayTime").val(); var pingCheckEnabled = $("#chkDhcpScopePingCheckEnabled").prop("checked"); var pingCheckTimeout = $("#txtDhcpScopePingCheckTimeout").val(); var pingCheckRetries = $("#txtDhcpScopePingCheckRetries").val(); var domainName = $("#txtDhcpScopeDomainName").val(); var domainSearchList = cleanTextList($("#txtDhcpScopeDomainSearchStrings").val()); var dnsUpdates = $("#chkDhcpScopeDnsUpdates").prop("checked"); var dnsOverwriteForDynamicLease = $("#chkDnsOverwriteForDynamicLease").prop("checked"); var dnsTtl = $("#txtDhcpScopeDnsTtl").val(); var serverAddress = $("#txtDhcpScopeServerAddress").val(); var serverHostName = $("#txtDhcpScopeServerHostName").val(); var bootFileName = $("#txtDhcpScopeBootFileName").val(); var routerAddress = $("#txtDhcpScopeRouterAddress").val(); var useThisDnsServer = $("#chkUseThisDnsServer").prop('checked'); var dnsServers = cleanTextList($("#txtDhcpScopeDnsServers").val()); var winsServers = cleanTextList($("#txtDhcpScopeWinsServers").val()); var ntpServers = cleanTextList($("#txtDhcpScopeNtpServers").val()); var ntpServerDomainNames = cleanTextList($("#txtDhcpScopeNtpServerDomainNames").val()); var staticRoutes = serializeTableData($("#tableDhcpScopeStaticRoutes"), 3); if (staticRoutes === false) return; var vendorInfo = serializeTableData($("#tableDhcpScopeVendorInfo"), 2); if (vendorInfo === false) return; var capwapAcIpAddresses = cleanTextList($("#txtDhcpScopeCAPWAPApIpAddresses").val()); var tftpServerAddresses = cleanTextList($("#txtDhcpScopeTftpServerAddresses").val()); var genericOptions = serializeTableData($("#tableDhcpScopeGenericOptions"), 2); if (genericOptions === false) return; var exclusions = serializeTableData($("#tableDhcpScopeExclusions"), 2); if (exclusions === false) return; var reservedLeases = serializeTableData($("#tableDhcpScopeReservedLeases"), 4); if (reservedLeases === false) return; var allowOnlyReservedLeases = $("#chkAllowOnlyReservedLeases").prop('checked'); var blockLocallyAdministeredMacAddresses = $("#chkBlockLocallyAdministeredMacAddresses").prop('checked'); var ignoreClientIdentifierOption = $("#chkIgnoreClientIdentifierOption").prop('checked'); var node = $("#optDhcpClusterNode").val(); var btn = $("#btnSaveDhcpScope"); btn.button("loading"); HTTPRequest({ url: "api/dhcp/scopes/set?token=" + sessionData.token + "&node=" + encodeURIComponent(node), method: "POST", data: "name=" + encodeURIComponent(name) + (newName == null ? "" : "&newName=" + encodeURIComponent(newName)) + "&startingAddress=" + encodeURIComponent(startingAddress) + "&endingAddress=" + encodeURIComponent(endingAddress) + "&subnetMask=" + encodeURIComponent(subnetMask) + "&leaseTimeDays=" + leaseTimeDays + "&leaseTimeHours=" + leaseTimeHours + "&leaseTimeMinutes=" + leaseTimeMinutes + "&offerDelayTime=" + offerDelayTime + "&pingCheckEnabled=" + pingCheckEnabled + "&pingCheckTimeout=" + pingCheckTimeout + "&pingCheckRetries=" + pingCheckRetries + "&domainName=" + encodeURIComponent(domainName) + "&domainSearchList=" + encodeURIComponent(domainSearchList) + "&dnsUpdates=" + dnsUpdates + "&dnsOverwriteForDynamicLease=" + dnsOverwriteForDynamicLease + "&dnsTtl=" + dnsTtl + "&serverAddress=" + encodeURIComponent(serverAddress) + "&serverHostName=" + encodeURIComponent(serverHostName) + "&bootFileName=" + encodeURIComponent(bootFileName) + "&routerAddress=" + encodeURIComponent(routerAddress) + "&useThisDnsServer=" + useThisDnsServer + (useThisDnsServer ? "" : "&dnsServers=" + encodeURIComponent(dnsServers)) + "&winsServers=" + encodeURIComponent(winsServers) + "&ntpServers=" + encodeURIComponent(ntpServers) + "&ntpServerDomainNames=" + encodeURIComponent(ntpServerDomainNames) + "&staticRoutes=" + encodeURIComponent(staticRoutes) + "&vendorInfo=" + encodeURIComponent(vendorInfo) + "&capwapAcIpAddresses=" + encodeURIComponent(capwapAcIpAddresses) + "&tftpServerAddresses=" + encodeURIComponent(tftpServerAddresses) + "&genericOptions=" + encodeURIComponent(genericOptions) + "&exclusions=" + encodeURIComponent(exclusions) + "&reservedLeases=" + encodeURIComponent(reservedLeases) + "&allowOnlyReservedLeases=" + allowOnlyReservedLeases + "&blockLocallyAdministeredMacAddresses=" + blockLocallyAdministeredMacAddresses + "&ignoreClientIdentifierOption=" + ignoreClientIdentifierOption, processData: false, success: function (responseJSON) { refreshDhcpScopes(); btn.button("reset"); showAlert("success", "Scope Saved!", "DHCP Scope was saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function disableDhcpScope(scopeName) { if (!confirm("Are you sure you want to disable the DHCP scope '" + scopeName + "'?")) return; var node = $("#optDhcpClusterNode").val(); var divDhcpViewScopesLoader = $("#divDhcpViewScopesLoader"); var divDhcpViewScopes = $("#divDhcpViewScopes"); var divDhcpEditScope = $("#divDhcpEditScope"); divDhcpViewScopes.hide(); divDhcpEditScope.hide(); divDhcpViewScopesLoader.show(); HTTPRequest({ url: "api/dhcp/scopes/disable?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshDhcpScopes(); showAlert("success", "Scope Disabled!", "DHCP Scope was disabled successfully."); }, error: function () { divDhcpViewScopesLoader.hide(); divDhcpViewScopes.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDhcpViewScopesLoader }); } function enableDhcpScope(scopeName) { var node = $("#optDhcpClusterNode").val(); var divDhcpViewScopesLoader = $("#divDhcpViewScopesLoader"); var divDhcpViewScopes = $("#divDhcpViewScopes"); var divDhcpEditScope = $("#divDhcpEditScope"); divDhcpViewScopes.hide(); divDhcpEditScope.hide(); divDhcpViewScopesLoader.show(); HTTPRequest({ url: "api/dhcp/scopes/enable?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshDhcpScopes(); showAlert("success", "Scope Enabled!", "DHCP Scope was enabled successfully."); }, error: function () { divDhcpViewScopesLoader.hide(); divDhcpViewScopes.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDhcpViewScopesLoader }); } function deleteDhcpScope(index, scopeName) { if (!confirm("Are you sure you want to delete the DHCP scope '" + scopeName + "'?")) return; var node = $("#optDhcpClusterNode").val(); var divDhcpViewScopesLoader = $("#divDhcpViewScopesLoader"); var divDhcpViewScopes = $("#divDhcpViewScopes"); var divDhcpEditScope = $("#divDhcpEditScope"); divDhcpViewScopes.hide(); divDhcpEditScope.hide(); divDhcpViewScopesLoader.show(); HTTPRequest({ url: "api/dhcp/scopes/delete?token=" + sessionData.token + "&name=" + encodeURIComponent(scopeName) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#trDhcpScopeRow" + index).remove(); var dhcpLeasesLength = $('#tableDhcpScopesBody >tr').length; if (dhcpLeasesLength > 0) $("#tableDhcpScopesFooter").html("Total Scopes: " + dhcpLeasesLength + ""); else $("#tableDhcpScopesFooter").html("No Scope Found"); divDhcpViewScopes.show(); divDhcpViewScopesLoader.hide(); showAlert("success", "Scope Deleted!", "DHCP Scope was deleted successfully."); }, error: function () { divDhcpViewScopesLoader.hide(); divDhcpViewScopes.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDhcpViewScopesLoader }); } ================================================ FILE: DnsServerCore/www/js/dnsclient.js ================================================ /* 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 . */ $(function () { loadServerList(); //dropdown list box support $('.dropdown').on('click', 'a', function (e) { e.preventDefault(); var itemText = $(this).text(); $(this).closest('.dropdown').find('input').val(itemText); if (itemText.indexOf("QUIC") !== -1) $("#optDnsClientProtocol").val("QUIC"); else if ((itemText.indexOf("TLS") !== -1) || (itemText.indexOf(":853") !== -1)) $("#optDnsClientProtocol").val("TLS"); else if ((itemText.indexOf("HTTPS") !== -1) || (itemText.indexOf("http://") !== -1) || (itemText.indexOf("https://") !== -1)) $("#optDnsClientProtocol").val("HTTPS"); else { switch ($("#optDnsClientProtocol").val()) { case "UDP": case "TCP": break; default: $("#optDnsClientProtocol").val("UDP"); break; } } }); }); function loadServerList() { $.ajax({ type: "GET", url: "json/dnsclient-server-list-custom.json", dataType: "json", cache: false, async: false, success: function (responseJSON, status, jqXHR) { loadServerListFrom(responseJSON); }, error: function (jqXHR, textStatus, errorThrown) { $.ajax({ type: "GET", url: "json/dnsclient-server-list-builtin.json", dataType: "json", cache: false, async: false, success: function (responseJSON, status, jqXHR) { loadServerListFrom(responseJSON); }, error: function (jqXHR, textStatus, errorThrown) { showAlert("danger", "Error!", "Failed to load server list: " + jqXHR.status + " " + jqXHR.statusText); } }); } }); } function loadServerListFrom(responseJSON) { $("#txtDnsClientNameServer").val("This Server {this-server}"); var htmlList = "
  • This Server {this-server}
  • "; for (var i = 0; i < responseJSON.length; i++) { for (var j = 0; j < responseJSON[i].addresses.length; j++) { if ((responseJSON[i].name == null) || (responseJSON[i].name.length == 0)) htmlList += "
  • " + htmlEncode(responseJSON[i].addresses[j]) + "
  • "; else htmlList += "
  • " + htmlEncode(responseJSON[i].name) + " {" + htmlEncode(responseJSON[i].addresses[j]) + "}
  • "; } } $("#optDnsClientNameServers").html(htmlList); } function resolveQuery(importRecords) { if (importRecords == null) importRecords = false; var server = $("#txtDnsClientNameServer").val(); if ((server.indexOf("recursive-resolver") !== -1) || (server.indexOf("system-dns") !== -1)) $("#optDnsClientProtocol").val("UDP"); var domain = $("#txtDnsClientDomain").val(); var type = $("#optDnsClientType").val(); var protocol = $("#optDnsClientProtocol").val(); var dnssecValidation = $("#chkDnsClientDnssecValidation").prop("checked"); var eDnsClientSubnet = $("#txtDnsClientEDnsClientSubnet").val(); { var i = server.indexOf("{"); if (i > -1) { var j = server.lastIndexOf("}"); server = server.substring(i + 1, j); } } server = server.trim(); if ((server === null) || (server === "")) { showAlert("warning", "Missing!", "Please enter a valid Name Server."); $("#txtDnsClientNameServer").trigger("focus"); return; } if ((domain === null) || (domain === "")) { showAlert("warning", "Missing!", "Please enter a domain name to query."); $("#txtDnsClientDomain").trigger("focus"); return; } { var i = domain.indexOf("://"); if (i > -1) { var j = domain.indexOf(":", i + 3); if (j < 0) j = domain.indexOf("/", i + 3); if (j > -1) domain = domain.substring(i + 3, j); else domain = domain.substring(i + 3); $("#txtDnsClientDomain").val(domain); } } if (importRecords) { if (!confirm("Importing all the records from the response of this query will add them into an existing primary or conditional forwarder zone. If a matching zone does not exists, a new primary zone for '" + domain + "' will be created.\n\nAre you sure you want to import all records?")) return; } var node = $("#optDnsClientClusterNode").val(); var btn = $(importRecords ? "#btnDnsClientImport" : "#btnDnsClientResolve").button("loading"); var btnOther = $(importRecords ? "#btnDnsClientResolve" : "#btnDnsClientImport").prop("disabled", true); var divDnsClientLoader = $("#divDnsClientLoader"); var divDnsClientOutputAccordion = $("#divDnsClientOutputAccordion"); divDnsClientOutputAccordion.hide(); divDnsClientLoader.show(); HTTPRequest({ url: "api/dnsClient/resolve?token=" + sessionData.token + "&server=" + encodeURIComponent(server) + "&domain=" + encodeURIComponent(domain) + "&type=" + type + "&protocol=" + protocol + "&dnssec=" + dnssecValidation + "&eDnsClientSubnet=" + encodeURIComponent(eDnsClientSubnet) + (importRecords ? "&import=true" : "") + "&node=" + encodeURIComponent(node), success: function (responseJSON) { divDnsClientLoader.hide(); btn.button("reset"); btnOther.prop("disabled", false); $("#preDnsClientFinalResponse").text(JSON.stringify(responseJSON.response.result, null, 2)); $("#divDnsClientFinalResponseCollapse").collapse("show"); $("#divDnsClientRawResponsesCollapse").collapse("hide"); divDnsClientOutputAccordion.show(); if ((responseJSON.response.rawResponses != null)) { if (responseJSON.response.rawResponses.length == 0) { $("#divDnsClientRawResponsePanel").hide(); } else { var rawListHtml = ""; for (var i = 0; i < responseJSON.response.rawResponses.length; i++) { rawListHtml += "
  • " + JSON.stringify(responseJSON.response.rawResponses[i], null, 2) + "
  • "; } $("#spanDnsClientRawResponsesCount").text(responseJSON.response.rawResponses.length); $("#ulDnsClientRawResponsesList").html(rawListHtml); $("#divDnsClientRawResponsesCollapse").collapse("hide"); $("#divDnsClientRawResponsePanel").show(); } } if (responseJSON.response.warningMessage != null) { showAlert("warning", "Warning!", responseJSON.response.warningMessage); } else if (importRecords) { showAlert("success", "Records Imported!", "Resource records resolved by this DNS client query were successfully imported into this server."); } }, error: function () { divDnsClientLoader.hide(); btn.button("reset"); btnOther.prop("disabled", false); }, invalidToken: function () { divDnsClientLoader.hide(); btn.button("reset"); btnOther.prop("disabled", false); showPageLogin(); }, objLoaderPlaceholder: divDnsClientLoader, showInnerError: true }); //add server name to list if doesnt exists var txtServerName = $("#txtDnsClientNameServer").val(); var containsServer = false; $("#optDnsClientNameServers a").each(function () { if ($(this).html() === txtServerName) containsServer = true; }); if (!containsServer) $("#optDnsClientNameServers").prepend("
  • " + htmlEncode(txtServerName) + "
  • "); } function queryDnsServer(domain, type, node) { if (type == null) type = "A"; $("#txtDnsClientNameServer").val("This Server {this-server}"); $("#txtDnsClientDomain").val(domain); $("#optDnsClientType").val(type); $("#optDnsClientProtocol").val("UDP"); $("#txtDnsClientEDnsClientSubnet").val(""); $("#chkDnsClientDnssecValidation").prop("checked", false); if ((node != null) && (node != "cluster")) $("#optDnsClientClusterNode").val(node); $("#mainPanelTabListDashboard").removeClass("active"); $("#mainPanelTabPaneDashboard").removeClass("active"); $("#mainPanelTabListLogs").removeClass("active"); $("#mainPanelTabPaneLogs").removeClass("active"); $("#mainPanelTabListDnsClient").addClass("active"); $("#mainPanelTabPaneDnsClient").addClass("active"); $("#modalTopStats").modal("hide"); resolveQuery(); } ================================================ FILE: DnsServerCore/www/js/logs.js ================================================ /* 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 . */ $(function () { $("#optQueryLogsAppName").on("change", function () { if (appsList == null) return; var appName = $("#optQueryLogsAppName").val(); var optClassPaths = ""; for (var i = 0; i < appsList.length; i++) { if (appsList[i].name == appName) { for (var j = 0; j < appsList[i].dnsApps.length; j++) { if (appsList[i].dnsApps[j].isQueryLogs) optClassPaths += ""; } break; } } $("#optQueryLogsClassPath").html(optClassPaths); $("#txtAddEditRecordDataData").val(""); }); $("#optQueryLogsEntriesPerPage").on("change", function () { localStorage.setItem("optQueryLogsEntriesPerPage", $("#optQueryLogsEntriesPerPage").val()); }); var optQueryLogsEntriesPerPage = localStorage.getItem("optQueryLogsEntriesPerPage"); if (optQueryLogsEntriesPerPage != null) $("#optQueryLogsEntriesPerPage").val(optQueryLogsEntriesPerPage); }); function refreshLogsTab() { if ($("#logsTabListLogViewer").hasClass("active")) refreshLogFilesList(); else if ($("#logsTabListQueryLogs").hasClass("active")) refreshQueryLogsTab(); } function logsClusterNodeChanged() { if ($("#logsTabListLogViewer").hasClass("active")) { if ($("#divLogViewer").is(":visible")) refreshLogFilesList($("#txtLogViewerTitle").text()); else refreshLogFilesList(); } else if ($("#logsTabListQueryLogs").hasClass("active")) { refreshQueryLogsTab(); if ($("#divQueryLogsTable").is(":visible")) queryLogs(); } } function refreshLogFilesList(selectedFileName) { var lstLogFiles = $("#lstLogFiles"); var node = $("#optLogsClusterNode").val(); HTTPRequest({ url: "api/logs/list?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var logFiles = responseJSON.response.logFiles; var list = ""; if (logFiles.length == 0) { list += "
    No Log File Was Found
    "; } else { list += ""; for (var i = 0; i < logFiles.length; i++) { var logFile = logFiles[i]; list += "" } } lstLogFiles.html(list); if (selectedFileName != null) { for (var i = 0; i < logFiles.length; i++) { if (logFiles[i].fileName == selectedFileName) { viewLog(selectedFileName); return; } } //selected file not found $("#divLogViewer").hide(); } }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: lstLogFiles }); } function viewLog(logFile) { var divLogViewer = $("#divLogViewer"); var txtLogViewerTitle = $("#txtLogViewerTitle"); var divLogViewerLoader = $("#divLogViewerLoader"); var preLogViewerBody = $("#preLogViewerBody"); txtLogViewerTitle.text(logFile); var node = $("#optLogsClusterNode").val(); preLogViewerBody.hide(); divLogViewerLoader.show(); divLogViewer.show(); HTTPRequest({ url: "api/logs/download?token=" + sessionData.token + "&fileName=" + encodeURIComponent(logFile) + "&limit=2" + "&node=" + encodeURIComponent(node), isTextResponse: true, success: function (response) { divLogViewerLoader.hide(); if (response.status != null) response = JSON.stringify(response, null, 2); preLogViewerBody.text(response); preLogViewerBody.show(); }, objLoaderPlaceholder: divLogViewerLoader }); } function downloadLog() { var logFile = $("#txtLogViewerTitle").text(); var node = $("#optLogsClusterNode").val(); window.open("api/logs/download?token=" + sessionData.token + "&fileName=" + encodeURIComponent(logFile) + "&node=" + encodeURIComponent(node) + "&ts=" + (new Date().getTime()), "_blank"); } function deleteLog() { var logFile = $("#txtLogViewerTitle").text(); if (!confirm("Are you sure you want to permanently delete the log file '" + logFile + "'?")) return; var node = $("#optLogsClusterNode").val(); var btn = $("#btnDeleteLog"); btn.button("loading"); HTTPRequest({ url: "api/logs/delete?token=" + sessionData.token + "&log=" + encodeURIComponent(logFile) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshLogFilesList(); $("#divLogViewer").hide(); btn.button("reset"); showAlert("success", "Log Deleted!", "Log file was deleted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function deleteAllLogs() { if (!confirm("Are you sure you want to permanently delete all log files?")) return; var node = $("#optLogsClusterNode").val(); HTTPRequest({ url: "api/logs/deleteAll?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshLogFilesList(); $("#divLogViewer").hide(); showAlert("success", "Logs Deleted!", "All log files were deleted successfully."); }, invalidToken: function () { showPageLogin(); } }); } function deleteAllStats() { if (!confirm("Are you sure you want to permanently delete all stats files?")) return; var node = $("#optLogsClusterNode").val(); HTTPRequest({ url: "api/dashboard/stats/deleteAll?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { showAlert("success", "Stats Deleted!", "All stats files were deleted successfully."); }, invalidToken: function () { showPageLogin(); } }); } var appsList; function refreshQueryLogsTab(doQueryLogs) { var frmQueryLogs = $("#frmQueryLogs"); var divQueryLogsLoader = $("#divQueryLogsLoader"); var optQueryLogsAppName = $("#optQueryLogsAppName"); var optQueryLogsClassPath = $("#optQueryLogsClassPath"); var currentAppName = optQueryLogsAppName.val(); var currentClassPath = optQueryLogsClassPath.val(); var loader; if (appsList == null) { frmQueryLogs.hide(); loader = divQueryLogsLoader; } else { optQueryLogsAppName.prop("disabled", true); optQueryLogsClassPath.prop("disabled", true); } HTTPRequest({ url: "api/apps/list?token=" + sessionData.token, success: function (responseJSON) { var apps = responseJSON.response.apps; var optApps = ""; var optClassPaths = ""; for (var i = 0; i < apps.length; i++) { for (var j = 0; j < apps[i].dnsApps.length; j++) { if (apps[i].dnsApps[j].isQueryLogs) { optApps += ""; if (currentAppName == null) currentAppName = apps[i].name; break; } } } for (var i = 0; i < apps.length; i++) { if (apps[i].name == currentAppName) { for (var j = 0; j < apps[i].dnsApps.length; j++) { if (apps[i].dnsApps[j].isQueryLogs) optClassPaths += ""; } break; } } optQueryLogsAppName.html(optApps); optQueryLogsClassPath.html(optClassPaths); if (currentAppName != null) optQueryLogsAppName.val(currentAppName); if (currentClassPath != null) optQueryLogsClassPath.val(currentClassPath); if (appsList == null) { frmQueryLogs.show(); loader.hide(); } else { optQueryLogsAppName.prop("disabled", false); optQueryLogsClassPath.prop("disabled", false); } appsList = apps; if (doQueryLogs) queryLogs(); }, error: function () { if (appsList == null) { frmQueryLogs.show(); } else { optQueryLogsAppName.prop("disabled", false); optQueryLogsClassPath.prop("disabled", false); } }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: loader }); } function queryLogs(pageNumber) { var btn = $("#btnQueryLogs"); var divQueryLogsLoader = $("#divQueryLogsLoader"); var divQueryLogsTable = $("#divQueryLogsTable"); var name = $("#optQueryLogsAppName").val(); if (name == null) { showAlert("warning", "Missing!", "Please install the 'Query Logs (Sqlite)' DNS App or any other DNS app that supports query logging feature from the Apps section."); $("#optQueryLogsAppName").trigger("focus"); return false; } var classPath = $("#optQueryLogsClassPath").val(); if (classPath == null) { showAlert("warning", "Missing!", "Please select a Class Path to query logs."); $("#optQueryLogsClassPath").trigger("focus"); return false; } if (pageNumber == null) pageNumber = $("#txtQueryLogPageNumber").val(); var entriesPerPage = Number($("#optQueryLogsEntriesPerPage").val()); if (entriesPerPage < 1) entriesPerPage = 10; var descendingOrder = $("#optQueryLogsDescendingOrder").val(); var start = $("#txtQueryLogStart").val(); if (start != "") start = moment(start).toISOString(); var end = $("#txtQueryLogEnd").val(); if (end != "") end = moment(end).toISOString(); var clientIpAddress = $("#txtQueryLogClientIpAddress").val(); var protocol = $("#optQueryLogsProtocol").val(); var responseType = $("#optQueryLogsResponseType").val(); var rcode = $("#optQueryLogsResponseCode").val(); var qname = $("#txtQueryLogQName").val(); var qtype = $("#txtQueryLogQType").val(); var qclass = $("#optQueryLogQClass").val(); var node = $("#optLogsClusterNode").val(); divQueryLogsTable.hide(); divQueryLogsLoader.show(); btn.button("loading"); HTTPRequest({ url: "api/logs/query?token=" + sessionData.token + "&name=" + encodeURIComponent(name) + "&classPath=" + encodeURIComponent(classPath) + "&pageNumber=" + pageNumber + "&entriesPerPage=" + entriesPerPage + "&descendingOrder=" + descendingOrder + "&start=" + encodeURIComponent(start) + "&end=" + encodeURIComponent(end) + "&clientIpAddress=" + encodeURIComponent(clientIpAddress) + "&protocol=" + protocol + "&responseType=" + responseType + "&rcode=" + rcode + "&qname=" + encodeURIComponent(qname) + "&qtype=" + qtype + "&qclass=" + qclass + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var tableHtml = ""; for (var i = 0; i < responseJSON.response.entries.length; i++) { var trbgcolor; switch (responseJSON.response.entries[i].rcode.toLowerCase()) { case "serverfailure": trbgcolor = "rgba(217, 83, 79, 0.1)"; break; case "nxdomain": switch (responseJSON.response.entries[i].responseType.toLowerCase()) { case "blocked": case "upstreamblocked": case "upstreamblockedcached": trbgcolor = "rgba(255, 165, 0, 0.1)"; break; default: trbgcolor = "rgba(120, 120, 120, 0.1)"; break; } break; case "refused": trbgcolor = "rgba(91, 192, 222, 0.1)"; break; default: switch (responseJSON.response.entries[i].responseType.toLowerCase()) { case "authoritative": trbgcolor = "rgba(150, 150, 0, 0.1)"; break; case "recursive": trbgcolor = "rgba(23, 162, 184, 0.1)"; break; case "cached": trbgcolor = "rgba(111, 84, 153, 0.1)"; break; case "blocked": case "upstreamblocked": case "upstreamblockedcached": trbgcolor = "rgba(255, 165, 0, 0.1)"; break; default: trbgcolor = null; break; } break; } tableHtml += "" + responseJSON.response.entries[i].rowNumber + "" + moment(responseJSON.response.entries[i].timestamp).local().format("YYYY-MM-DD HH:mm:ss") + "" + responseJSON.response.entries[i].clientIpAddress + "" + responseJSON.response.entries[i].protocol + "" + responseJSON.response.entries[i].responseType + (responseJSON.response.entries[i].responseRtt == null ? "" : "
    (" + responseJSON.response.entries[i].responseRtt.toFixed(2) + " ms)
    ") + "" + responseJSON.response.entries[i].rcode + "" + htmlEncode(responseJSON.response.entries[i].qname == "" ? "." : responseJSON.response.entries[i].qname) + "" + (responseJSON.response.entries[i].qtype == null ? "" : responseJSON.response.entries[i].qtype) + "" + (responseJSON.response.entries[i].qclass == null ? "" : responseJSON.response.entries[i].qclass) + "" + htmlEncode(responseJSON.response.entries[i].answer) + "
      "; tableHtml += "
    • Query DNS Server
    • "; switch (responseJSON.response.entries[i].responseType.toLowerCase()) { case "blocked": case "upstreamblocked": case "upstreamblockedcached": tableHtml += "
    • Allow Domain
    • "; break; default: tableHtml += "
    • Block Domain
    • "; break; } tableHtml += "
    "; } var paginationHtml = ""; if (responseJSON.response.pageNumber > 1) { paginationHtml += "
  • «
  • "; paginationHtml += "
  • "; } var pageStart = responseJSON.response.pageNumber - 5; if (pageStart < 1) pageStart = 1; var pageEnd = pageStart + 9; if (pageEnd > responseJSON.response.totalPages) { var endDiff = pageEnd - responseJSON.response.totalPages; pageEnd = responseJSON.response.totalPages; pageStart -= endDiff; if (pageStart < 1) pageStart = 1; } for (var i = pageStart; i <= pageEnd; i++) { if (i == responseJSON.response.pageNumber) paginationHtml += "
  • " + i + "
  • "; else paginationHtml += "
  • " + i + "
  • "; } if (responseJSON.response.pageNumber < responseJSON.response.totalPages) { paginationHtml += "
  • "; paginationHtml += "
  • »
  • "; } $("#tableQueryLogsBody").html(tableHtml); var statusHtml; if (responseJSON.response.entries.length > 0) statusHtml = responseJSON.response.entries[0].rowNumber + "-" + responseJSON.response.entries[responseJSON.response.entries.length - 1].rowNumber + " (" + responseJSON.response.entries.length + ") of " + responseJSON.response.totalEntries + " logs (page " + responseJSON.response.pageNumber + " of " + responseJSON.response.totalPages + ")"; else statusHtml = "0 logs"; $("#tableQueryLogsTopStatus").html(statusHtml); $("#tableQueryLogsTopPagination").html(paginationHtml); $("#tableQueryLogsFooterStatus").html(statusHtml); $("#tableQueryLogsFooterPagination").html(paginationHtml); btn.button("reset"); divQueryLogsLoader.hide(); divQueryLogsTable.show(); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); }, objLoaderPlaceholder: divQueryLogsLoader }); } function showQueryLogs(domain, clientIp, node) { $("#frmQueryLogs").trigger("reset"); if (domain != null) $("#txtQueryLogQName").val(domain); if (clientIp != null) $("#txtQueryLogClientIpAddress").val(clientIp); if ((node != null) && (node != "cluster")) $("#optLogsClusterNode").val(node); $("#mainPanelTabListDashboard").removeClass("active"); $("#mainPanelTabPaneDashboard").removeClass("active"); $("#mainPanelTabListLogs").addClass("active"); $("#mainPanelTabPaneLogs").addClass("active"); $("#logsTabListLogViewer").removeClass("active"); $("#logsTabPaneLogViewer").removeClass("active"); $("#logsTabListQueryLogs").addClass("active"); $("#logsTabPaneQueryLogs").addClass("active"); $("#modalTopStats").modal("hide"); refreshQueryLogsTab(true); } function exportQueryLogsCsv() { var name = $("#optQueryLogsAppName").val(); if (name == null) { showAlert("warning", "Missing!", "Please install the 'Query Logs (Sqlite)' DNS App or any other DNS app that supports query logging feature."); $("#optQueryLogsAppName").trigger("focus"); return false; } var classPath = $("#optQueryLogsClassPath").val(); if (classPath == null) { showAlert("warning", "Missing!", "Please select a Class Path to query logs."); $("#optQueryLogsClassPath").trigger("focus"); return false; } var start = $("#txtQueryLogStart").val(); if (start != "") start = moment(start).toISOString(); var end = $("#txtQueryLogEnd").val(); if (end != "") end = moment(end).toISOString(); var clientIpAddress = $("#txtQueryLogClientIpAddress").val(); var protocol = $("#optQueryLogsProtocol").val(); var responseType = $("#optQueryLogsResponseType").val(); var rcode = $("#optQueryLogsResponseCode").val(); var qname = $("#txtQueryLogQName").val(); var qtype = $("#txtQueryLogQType").val(); var qclass = $("#optQueryLogQClass").val(); var node = $("#optLogsClusterNode").val(); window.open("api/logs/export?token=" + sessionData.token + "&name=" + encodeURIComponent(name) + "&classPath=" + encodeURIComponent(classPath) + "&start=" + encodeURIComponent(start) + "&end=" + encodeURIComponent(end) + "&clientIpAddress=" + encodeURIComponent(clientIpAddress) + "&protocol=" + protocol + "&responseType=" + responseType + "&rcode=" + rcode + "&qname=" + encodeURIComponent(qname) + "&qtype=" + qtype + "&qclass=" + qclass + "&node=" + encodeURIComponent(node) , "_blank"); } ================================================ FILE: DnsServerCore/www/js/main.js ================================================ /* 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 . */ var refreshTimerHandle; var reverseProxyDetected = false; var quickBlockLists = null; var quickForwardersList = null; function showPageLogin() { hideAlert(); localStorage.removeItem("token"); $("#pageMain").hide(); $("#mnuUser").hide(); $("#txtUser").val(""); $("#txtPass").val(""); $("#txtPass").prop("disabled", false); $("#div2FAOTP").hide(); $("#txt2FATOTP").val(""); $("#btnLogin").button("reset"); $("#pageLogin").show(); $("#txtUser").trigger("focus"); if (refreshTimerHandle != null) { clearInterval(refreshTimerHandle); refreshTimerHandle = null; } } function showPageMain() { hideAlert(); $("#pageLogin").hide(); $("#mnuUser").show(); $(".nav-tabs li").removeClass("active"); $(".tab-pane").removeClass("active"); $("#mainPanelTabListDashboard").addClass("active"); $("#mainPanelTabPaneDashboard").addClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); $("#dhcpTabListLeases").addClass("active"); $("#dhcpTabPaneLeases").addClass("active"); $("#adminTabListSessions").addClass("active"); $("#adminTabPaneSessions").addClass("active"); $("#logsTabListLogViewer").addClass("active"); $("#logsTabPaneLogViewer").addClass("active"); $("#divViewZones").show(); $("#divEditZone").hide(); $("#divDhcpViewScopes").show(); $("#divDhcpEditScope").hide(); $("#txtDnsClientNameServer").val("This Server {this-server}"); $("#txtDnsClientDomain").val(""); $("#optDnsClientType").val("A"); $("#optDnsClientProtocol").val("UDP"); $("#txtDnsClientEDnsClientSubnet").val(""); $("#chkDnsClientDnssecValidation").prop("checked", false); $("#divDnsClientLoader").hide(); $("#preDnsClientFinalResponse").text(""); $("#divDnsClientOutputAccordion").hide(); $("#divLogViewer").hide(); $("#divQueryLogsTable").hide(); updateAllClusterNodeDropDowns(); if (sessionData.info.permissions.Dashboard.canView) { $("#mainPanelTabListDashboard").show(); refreshDashboard(); } else { $("#mainPanelTabListDashboard").hide(); $("#mainPanelTabListDashboard").removeClass("active"); $("#mainPanelTabPaneDashboard").removeClass("active"); if (sessionData.info.permissions.Zones.canView) { $("#mainPanelTabListZones").addClass("active"); $("#mainPanelTabPaneZones").addClass("active"); refreshZones(true); } else if (sessionData.info.permissions.Cache.canView) { $("#mainPanelTabListCachedZones").addClass("active"); $("#mainPanelTabPaneCachedZones").addClass("active"); } else if (sessionData.info.permissions.Allowed.canView) { $("#mainPanelTabListAllowedZones").addClass("active"); $("#mainPanelTabPaneAllowedZones").addClass("active"); } else if (sessionData.info.permissions.Blocked.canView) { $("#mainPanelTabListBlockedZones").addClass("active"); $("#mainPanelTabPaneBlockedZones").addClass("active"); } else if (sessionData.info.permissions.Apps.canView) { $("#mainPanelTabListApps").addClass("active"); $("#mainPanelTabPaneApps").addClass("active"); refreshApps(); } else if (sessionData.info.permissions.DnsClient.canView) { $("#mainPanelTabListDnsClient").addClass("active"); $("#mainPanelTabPaneDnsClient").addClass("active"); } else if (sessionData.info.permissions.Settings.canView) { $("#mainPanelTabListSettings").addClass("active"); $("#mainPanelTabPaneSettings").addClass("active"); refreshDnsSettings() } else if (sessionData.info.permissions.DhcpServer.canView) { $("#mainPanelTabListDhcp").addClass("active"); $("#mainPanelTabPaneDhcp").addClass("active"); refreshDhcpTab(); } else if (sessionData.info.permissions.Administration.canView) { $("#mainPanelTabListAdmin").addClass("active"); $("#mainPanelTabPaneAdmin").addClass("active"); refreshAdminTab(); } else if (sessionData.info.permissions.Logs.canView) { $("#mainPanelTabListLogs").addClass("active"); $("#mainPanelTabPaneLogs").addClass("active"); refreshLogsTab(); } else { $("#mainPanelTabListAbout").addClass("active"); $("#mainPanelTabPaneAbout").addClass("active"); } } if (sessionData.info.permissions.Zones.canView) { $("#mainPanelTabListZones").show(); } else { $("#mainPanelTabListZones").hide(); } if (sessionData.info.permissions.Cache.canView) { $("#mainPanelTabListCachedZones").show(); refreshCachedZonesList(""); } else { $("#mainPanelTabListCachedZones").hide(); } if (sessionData.info.permissions.Allowed.canView) { $("#mainPanelTabListAllowedZones").show(); refreshAllowedZonesList(""); } else { $("#mainPanelTabListAllowedZones").hide(); } if (sessionData.info.permissions.Blocked.canView) { $("#mainPanelTabListBlockedZones").show(); refreshBlockedZonesList(""); } else { $("#mainPanelTabListBlockedZones").hide(); } if (sessionData.info.permissions.Apps.canView) { $("#mainPanelTabListApps").show(); } else { $("#mainPanelTabListApps").hide(); } if (sessionData.info.permissions.DnsClient.canView) { $("#mainPanelTabListDnsClient").show(); } else { $("#mainPanelTabListDnsClient").hide(); } if (sessionData.info.permissions.Settings.canView) { $("#mainPanelTabListSettings").show(); } else { $("#mainPanelTabListSettings").hide(); } if (sessionData.info.permissions.DhcpServer.canView) { $("#mainPanelTabListDhcp").show(); } else { $("#mainPanelTabListDhcp").hide(); } if (sessionData.info.permissions.Administration.canView) { $("#mainPanelTabListAdmin").show(); } else { $("#mainPanelTabListAdmin").hide(); } if (sessionData.info.permissions.Logs.canView) { $("#mainPanelTabListLogs").show(); } else { $("#mainPanelTabListLogs").hide(); } $("#pageMain").show(); checkForUpdate(); refreshTimerHandle = setInterval(function () { var type = $("input[name=rdStatType]:checked").val(); if (type === "lastHour") refreshDashboard(true); $("#lblAboutUptime").text(moment(sessionData.info.uptimestamp).local().format("lll") + " (" + moment(sessionData.info.uptimestamp).fromNow() + ")"); }, 60000); } $(function () { var headerHtml = $("#header").html(); $("#header").html("
    \"TechnitiumTechnitium" + headerHtml + "
    "); $("#footer").html(""); loadQuickBlockLists(); loadQuickForwardersList(); $("#chkEnableUdpSocketPool").on("click", function () { var enableUdpSocketPool = $("#chkEnableUdpSocketPool").prop("checked"); $("#txtUdpSocketPoolExcludedPorts").prop("disabled", !enableUdpSocketPool); }); $("#chkEDnsClientSubnet").on("click", function () { var eDnsClientSubnet = $("#chkEDnsClientSubnet").prop("checked"); $("#txtEDnsClientSubnetIPv4PrefixLength").prop("disabled", !eDnsClientSubnet); $("#txtEDnsClientSubnetIPv6PrefixLength").prop("disabled", !eDnsClientSubnet); $("#txtEDnsClientSubnetIpv4Override").prop("disabled", !eDnsClientSubnet); $("#txtEDnsClientSubnetIpv6Override").prop("disabled", !eDnsClientSubnet); }); $("#chkEnableBlocking").on("click", updateBlockingState); $("input[type=radio][name=rdProxyType]").on("change", function () { var proxyType = $("input[name=rdProxyType]:checked").val().toLowerCase(); if (proxyType === "none") { $("#txtProxyAddress").prop("disabled", true); $("#txtProxyPort").prop("disabled", true); $("#txtProxyUsername").prop("disabled", true); $("#txtProxyPassword").prop("disabled", true); $("#txtProxyBypassList").prop("disabled", true); } else { $("#txtProxyAddress").prop("disabled", false); $("#txtProxyPort").prop("disabled", false); $("#txtProxyUsername").prop("disabled", false); $("#txtProxyPassword").prop("disabled", false); $("#txtProxyBypassList").prop("disabled", false); } }); $("input[type=radio][name=rdRecursion]").on("change", function () { var recursion = $("input[name=rdRecursion]:checked").val(); $("#txtRecursionNetworkACL").prop("disabled", recursion !== "UseSpecifiedNetworkACL"); }); $("input[type=radio][name=rdBlockingType]").on("change", function () { var recursion = $("input[name=rdBlockingType]:checked").val(); if (recursion === "CustomAddress") { $("#txtCustomBlockingAddresses").prop("disabled", false); } else { $("#txtCustomBlockingAddresses").prop("disabled", true); } }); $("#chkWebServiceEnableTls").on("click", function () { var webServiceEnableTls = $("#chkWebServiceEnableTls").prop("checked"); $("#chkWebServiceEnableHttp3").prop("disabled", !webServiceEnableTls); $("#chkWebServiceHttpToTlsRedirect").prop("disabled", !webServiceEnableTls); $("#chkWebServiceUseSelfSignedTlsCertificate").prop("disabled", !webServiceEnableTls); $("#txtWebServiceTlsPort").prop("disabled", !webServiceEnableTls); $("#txtWebServiceTlsCertificatePath").prop("disabled", !webServiceEnableTls); $("#txtWebServiceTlsCertificatePassword").prop("disabled", !webServiceEnableTls); }); $("#chkEnableDnsOverUdpProxy").on("click", function () { var enableDnsOverUdpProxy = $("#chkEnableDnsOverUdpProxy").prop("checked"); var enableDnsOverTcpProxy = $("#chkEnableDnsOverTcpProxy").prop("checked"); var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); $("#txtDnsOverUdpProxyPort").prop("disabled", !enableDnsOverUdpProxy); $("#txtReverseProxyNetworkACL").prop("disabled", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps); }); $("#chkEnableDnsOverTcpProxy").on("click", function () { var enableDnsOverUdpProxy = $("#chkEnableDnsOverUdpProxy").prop("checked"); var enableDnsOverTcpProxy = $("#chkEnableDnsOverTcpProxy").prop("checked"); var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); $("#txtDnsOverTcpProxyPort").prop("disabled", !enableDnsOverTcpProxy); $("#txtReverseProxyNetworkACL").prop("disabled", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps); }); $("#chkEnableDnsOverHttp").on("click", function () { var enableDnsOverUdpProxy = $("#chkEnableDnsOverUdpProxy").prop("checked"); var enableDnsOverTcpProxy = $("#chkEnableDnsOverTcpProxy").prop("checked"); var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); $("#txtDnsOverHttpPort").prop("disabled", !enableDnsOverHttp); $("#txtReverseProxyNetworkACL").prop("disabled", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps); $("#txtDnsOverHttpRealIpHeader").prop("disabled", !enableDnsOverHttp && !enableDnsOverHttps); }); $("#chkEnableDnsOverTls").on("click", function () { var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); $("#txtDnsOverTlsPort").prop("disabled", !enableDnsOverTls); $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); }); $("#chkEnableDnsOverHttps").on("click", function () { var enableDnsOverUdpProxy = $("#chkEnableDnsOverUdpProxy").prop("checked"); var enableDnsOverTcpProxy = $("#chkEnableDnsOverTcpProxy").prop("checked"); var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); $("#chkEnableDnsOverHttp3").prop("disabled", !enableDnsOverHttps); $("#txtDnsOverHttpsPort").prop("disabled", !enableDnsOverHttps); $("#txtReverseProxyNetworkACL").prop("disabled", !enableDnsOverUdpProxy && !enableDnsOverTcpProxy && !enableDnsOverHttp && !enableDnsOverHttps); $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); $("#txtDnsOverHttpRealIpHeader").prop("disabled", !enableDnsOverHttp && !enableDnsOverHttps); }); $("#chkEnableDnsOverQuic").on("click", function () { var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); $("#txtDnsOverQuicPort").prop("disabled", !enableDnsOverQuic); $("#txtDnsTlsCertificatePath").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); $("#txtDnsTlsCertificatePassword").prop("disabled", !enableDnsOverTls && !enableDnsOverHttps && !enableDnsOverQuic); }); $("#chkEnableConcurrentForwarding").on("click", function () { var concurrentForwarding = $("#chkEnableConcurrentForwarding").prop("checked"); $("#txtForwarderConcurrency").prop("disabled", !concurrentForwarding) }); $("input[type=radio][name=rdLoggingType]").on("change", function () { var rdLoggingType = $("input[name=rdLoggingType]:checked").val(); var enableLogging = rdLoggingType.toLowerCase() != "none"; $("#chkIgnoreResolverLogs").prop("disabled", !enableLogging); $("#chkLogQueries").prop("disabled", !enableLogging); $("#chkUseLocalTime").prop("disabled", !enableLogging); $("#txtLogFolderPath").prop("disabled", !enableLogging); }); $("#chkServeStale").on("click", function () { var serveStale = $("#chkServeStale").prop("checked"); $("#txtServeStaleTtl").prop("disabled", !serveStale); $("#txtServeStaleAnswerTtl").prop("disabled", !serveStale); $("#txtServeStaleResetTtl").prop("disabled", !serveStale); $("#txtServeStaleMaxWaitTime").prop("disabled", !serveStale); }); $("#optQuickBlockList").on("change", function () { var selectedOption = $("#optQuickBlockList").val(); switch (selectedOption) { case "blank": break; case "none": $("#txtBlockListUrls").val(""); break; default: for (var i = 0; i < quickBlockLists.length; i++) { if (quickBlockLists[i].name === selectedOption) { var existingList; if (selectedOption.toLowerCase() == "default") existingList = ""; else existingList = $("#txtBlockListUrls").val(); var newList = existingList; for (var j = 0; j < quickBlockLists[i].urls.length; j++) { var url = quickBlockLists[i].urls[j]; if (existingList.indexOf(url) < 0) newList += url + "\n"; } $("#txtBlockListUrls").val(newList); break; } } break; } }); $("#optQuickForwarders").on("change", function () { var selectedOption = $("#optQuickForwarders").val(); switch (selectedOption) { case "blank": break; case "none": $("#txtForwarders").val(""); $("#rdForwarderProtocolUdp").prop("checked", true); break; default: for (var i = 0; i < quickForwardersList.length; i++) { if (quickForwardersList[i].name === selectedOption) { var forwarders = ""; for (var j = 0; j < quickForwardersList[i].addresses.length; j++) { forwarders += quickForwardersList[i].addresses[j] + "\n"; } $("#txtForwarders").val(forwarders); switch (quickForwardersList[i].protocol.toUpperCase()) { case "TCP": $("#rdForwarderProtocolTcp").prop("checked", true); break; case "TLS": $("#rdForwarderProtocolTls").prop("checked", true); break; case "HTTPS": $("#rdForwarderProtocolHttps").prop("checked", true); break; case "QUIC": $("#rdForwarderProtocolQuic").prop("checked", true); break; default: $("#rdForwarderProtocolUdp").prop("checked", true); break; } if (quickForwardersList[i].proxyType == null) quickForwardersList[i].proxyType = "DefaultProxy"; switch (quickForwardersList[i].proxyType.toUpperCase()) { case "SOCKS5": case "HTTP": if (quickForwardersList[i].proxyType.toUpperCase() == "SOCKS5") $("#rdProxyTypeSocks5").prop("checked", true); else $("#rdProxyTypeHttp").prop("checked", true); $("#txtProxyAddress").val(quickForwardersList[i].proxyAddress); $("#txtProxyPort").val(quickForwardersList[i].proxyPort); $("#txtProxyUsername").val(quickForwardersList[i].proxyUsername); $("#txtProxyPassword").val(quickForwardersList[i].proxyPassword); $("#txtProxyAddress").prop("disabled", false); $("#txtProxyPort").prop("disabled", false); $("#txtProxyUsername").prop("disabled", false); $("#txtProxyPassword").prop("disabled", false); break; case "NONE": $("#rdProxyTypeNone").prop("checked", true); $("#txtProxyAddress").prop("disabled", true); $("#txtProxyPort").prop("disabled", true); $("#txtProxyUsername").prop("disabled", true); $("#txtProxyPassword").prop("disabled", true); $("#txtProxyAddress").val(""); $("#txtProxyPort").val(""); $("#txtProxyUsername").val(""); $("#txtProxyPassword").val(""); break; } break; } } break; } }); $("input[type=radio][name=rdStatType]").on("change", function () { var type = $("input[name=rdStatType]:checked").val(); if (type === "custom") { $("#divCustomDayWise").show(); if ($("#dpCustomDayWiseStart").val() === "") { $("#dpCustomDayWiseStart").trigger("focus"); return; } if ($("#dpCustomDayWiseEnd").val() === "") { $("#dpCustomDayWiseEnd").trigger("focus"); return; } refreshDashboard(); } else { $("#divCustomDayWise").hide(); refreshDashboard(); } }); $("#btnCustomDayWise").on("click", function () { refreshDashboard(); }); applyTheme(); }); function showAbout() { if ($("#pageLogin").is(":visible")) { window.open("https://technitium.com/aboutus.html", "_blank"); } else { $("#mainPanelTabListDashboard").removeClass("active"); $("#mainPanelTabPaneDashboard").removeClass("active"); $("#mainPanelTabListZones").removeClass("active"); $("#mainPanelTabPaneZones").removeClass("active"); $("#mainPanelTabListCachedZones").removeClass("active"); $("#mainPanelTabPaneCachedZones").removeClass("active"); $("#mainPanelTabListAllowedZones").removeClass("active"); $("#mainPanelTabPaneAllowedZones").removeClass("active"); $("#mainPanelTabListBlockedZones").removeClass("active"); $("#mainPanelTabPaneBlockedZones").removeClass("active"); $("#mainPanelTabListApps").removeClass("active"); $("#mainPanelTabPaneApps").removeClass("active"); $("#mainPanelTabListDnsClient").removeClass("active"); $("#mainPanelTabPaneDnsClient").removeClass("active"); $("#mainPanelTabListSettings").removeClass("active"); $("#mainPanelTabPaneSettings").removeClass("active"); $("#mainPanelTabListDhcp").removeClass("active"); $("#mainPanelTabPaneDhcp").removeClass("active"); $("#mainPanelTabListAdmin").removeClass("active"); $("#mainPanelTabPaneAdmin").removeClass("active"); $("#mainPanelTabListLogs").removeClass("active"); $("#mainPanelTabPaneLogs").removeClass("active"); $("#mainPanelTabListAbout").addClass("active"); $("#mainPanelTabPaneAbout").addClass("active"); setTimeout(function () { window.scroll({ top: 0, left: 0, behavior: "smooth" }); }, 500); } } function checkForUpdate() { HTTPRequest({ url: "api/user/checkForUpdate?token=" + sessionData.token, success: function (responseJSON) { var lnkUpdateAvailable = $("#lnkUpdateAvailable"); if (responseJSON.response.updateAvailable) { $("#lblUpdateVersion").text(responseJSON.response.updateVersion); $("#lblCurrentVersion").text(responseJSON.response.currentVersion); if (responseJSON.response.updateTitle == null) responseJSON.response.updateTitle = "New Update Available!"; lnkUpdateAvailable.text(responseJSON.response.updateTitle); $("#lblUpdateAvailableTitle").text(responseJSON.response.updateTitle); var lblUpdateMessage = $("#lblUpdateMessage"); var lnkUpdateDownload = $("#lnkUpdateDownload"); var lnkUpdateInstructions = $("#lnkUpdateInstructions"); var lnkUpdateChangeLog = $("#lnkUpdateChangeLog"); if (responseJSON.response.updateMessage == null) { lblUpdateMessage.hide(); } else { lblUpdateMessage.text(responseJSON.response.updateMessage); lblUpdateMessage.show(); } if (responseJSON.response.downloadLink == null) { lnkUpdateDownload.hide(); } else { lnkUpdateDownload.attr("href", responseJSON.response.downloadLink); lnkUpdateDownload.show(); } if (responseJSON.response.instructionsLink == null) { lnkUpdateInstructions.hide(); } else { lnkUpdateInstructions.attr("href", responseJSON.response.instructionsLink); lnkUpdateInstructions.show(); } if (responseJSON.response.changeLogLink == null) { lnkUpdateChangeLog.hide(); } else { lnkUpdateChangeLog.attr("href", responseJSON.response.changeLogLink); lnkUpdateChangeLog.show(); } lnkUpdateAvailable.show(); } else { lnkUpdateAvailable.hide(); } }, invalidToken: function () { showPageLogin(); } }); } function loadQuickBlockLists() { $.ajax({ type: "GET", url: "json/quick-block-lists-custom.json", dataType: "json", cache: false, async: false, success: function (responseJSON, status, jqXHR) { loadQuickBlockListsFrom(responseJSON); }, error: function (jqXHR, textStatus, errorThrown) { $.ajax({ type: "GET", url: "json/quick-block-lists-builtin.json", dataType: "json", cache: false, async: false, success: function (responseJSON, status, jqXHR) { loadQuickBlockListsFrom(responseJSON); }, error: function (jqXHR, textStatus, errorThrown) { showAlert("danger", "Error!", "Failed to load Quick Forwarders list: " + jqXHR.status + " " + jqXHR.statusText); } }); } }); } function loadQuickBlockListsFrom(responseJSON) { var htmlList = ""; for (var i = 0; i < responseJSON.length; i++) { htmlList += ""; } quickBlockLists = responseJSON; $("#optQuickBlockList").html(htmlList); } function loadQuickForwardersList() { $.ajax({ type: "GET", url: "json/quick-forwarders-list-custom.json", dataType: "json", cache: false, async: false, success: function (responseJSON, status, jqXHR) { loadQuickForwardersListFrom(responseJSON); }, error: function (jqXHR, textStatus, errorThrown) { $.ajax({ type: "GET", url: "json/quick-forwarders-list-builtin.json", dataType: "json", cache: false, async: false, success: function (responseJSON, status, jqXHR) { loadQuickForwardersListFrom(responseJSON); }, error: function (jqXHR, textStatus, errorThrown) { showAlert("danger", "Error!", "Failed to load Quick Forwarders list: " + jqXHR.status + " " + jqXHR.statusText); } }); } }); } function loadQuickForwardersListFrom(responseJSON) { var htmlList = ""; for (var i = 0; i < responseJSON.length; i++) { htmlList += ""; } quickForwardersList = responseJSON; $("#optQuickForwarders").html(htmlList); } function refreshDnsSettings() { var divDnsSettingsLoader = $("#divDnsSettingsLoader"); var divDnsSettings = $("#divDnsSettings"); var node = $("#optSettingsClusterNode").val(); divDnsSettings.hide(); divDnsSettingsLoader.show(); HTTPRequest({ url: "api/settings/get?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { if ((node == "") || (node == "cluster") || (node == sessionData.info.dnsServerDomain)) updateDnsSettingsDataAndGui(responseJSON); loadDnsSettings(responseJSON); checkForReverseProxy(responseJSON); if (node == "cluster") { //cluster view //general $("#divSettingsGeneralLocalParameters").hide(); $("#divSettingsGeneralDefaultParameters").show(); $("#divSettingsGeneralDnsApps").show(); $("#divSettingsGeneralIpv6").hide(); $("#divSettingsGeneralUdpSocketPool").hide(); $("#divSettingsGeneralEDns").show(); $("#divSettingsGeneralDnssec").show(); $("#divSettingsGeneralEDnsClientSubnet").show(); $("#divSettingsGeneralRateLimiting").show(); $("#divSettingsGeneralAdvancedOptions").show(); //web service $("#settingsTabListWebService").hide(); if ($("#settingsTabListWebService").hasClass("active")) { $("#settingsTabListWebService").removeClass("active"); $("#settingsTabPaneWebService").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //optional protocols $("#settingsTabListOptionalProtocols").hide(); if ($("#settingsTabListOptionalProtocols").hasClass("active")) { $("#settingsTabListOptionalProtocols").removeClass("active"); $("#settingsTabPaneOptionalProtocols").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //tsig $("#settingsTabListTsig").show(); //recursion $("#settingsTabListRecursion").show(); //cache $("#settingsTabListCache").hide(); if ($("#settingsTabListCache").hasClass("active")) { $("#settingsTabListCache").removeClass("active"); $("#settingsTabPaneCache").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //blocking $("#settingsTabListBlocking").show(); //proxy & forwarders $("#settingsTabListProxyForwarders").show(); //logging $("#settingsTabListLogging").hide(); if ($("#settingsTabListLogging").hasClass("active")) { $("#settingsTabListLogging").removeClass("active"); $("#settingsTabPaneLogging").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //buttons $("#btnSettingsFlushCache").hide(); $("#btnShowBackupSettingsModal").hide(); $("#btnShowRestoreSettingsModal").hide(); } else if (node != "") { //node view //general $("#divSettingsGeneralLocalParameters").show(); $("#divSettingsGeneralDefaultParameters").hide(); $("#divSettingsGeneralDnsApps").hide(); $("#divSettingsGeneralIpv6").show(); $("#divSettingsGeneralUdpSocketPool").show(); $("#divSettingsGeneralEDns").hide(); $("#divSettingsGeneralDnssec").hide(); $("#divSettingsGeneralEDnsClientSubnet").hide(); $("#divSettingsGeneralRateLimiting").hide(); $("#divSettingsGeneralAdvancedOptions").hide(); //web service $("#settingsTabListWebService").show(); //optional protocols $("#settingsTabListOptionalProtocols").show(); //tsig $("#settingsTabListTsig").hide(); if ($("#settingsTabListTsig").hasClass("active")) { $("#settingsTabListTsig").removeClass("active"); $("#settingsTabPaneTsig").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //recursion $("#settingsTabListRecursion").hide(); if ($("#settingsTabListRecursion").hasClass("active")) { $("#settingsTabListRecursion").removeClass("active"); $("#settingsTabPaneRecursion").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //cache $("#settingsTabListCache").show(); //blocking $("#settingsTabListBlocking").hide(); if ($("#settingsTabListBlocking").hasClass("active")) { $("#settingsTabListBlocking").removeClass("active"); $("#settingsTabPaneBlocking").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //proxy & forwarders $("#settingsTabListProxyForwarders").hide(); if ($("#settingsTabListProxyForwarders").hasClass("active")) { $("#settingsTabListProxyForwarders").removeClass("active"); $("#settingsTabPaneProxyForwarders").removeClass("active"); $("#settingsTabListGeneral").addClass("active"); $("#settingsTabPaneGeneral").addClass("active"); } //logging $("#settingsTabListLogging").show(); //buttons $("#btnSettingsFlushCache").show(); $("#btnShowBackupSettingsModal").show(); $("#btnShowRestoreSettingsModal").show(); } else { //clustering disabled //general $("#divSettingsGeneralLocalParameters").show(); $("#divSettingsGeneralDefaultParameters").show(); $("#divSettingsGeneralDnsApps").show(); $("#divSettingsGeneralIpv6").show(); $("#divSettingsGeneralUdpSocketPool").show(); $("#divSettingsGeneralEDns").show(); $("#divSettingsGeneralDnssec").show(); $("#divSettingsGeneralEDnsClientSubnet").show(); $("#divSettingsGeneralRateLimiting").show(); $("#divSettingsGeneralAdvancedOptions").show(); //web service $("#settingsTabListWebService").show(); //optional protocols $("#settingsTabListOptionalProtocols").show(); //tsig $("#settingsTabListTsig").show(); //recursion $("#settingsTabListRecursion").show(); //cache $("#settingsTabListCache").show(); //blocking $("#settingsTabListBlocking").show(); //proxy & forwarders $("#settingsTabListProxyForwarders").show(); //logging $("#settingsTabListLogging").show(); //buttons $("#btnSettingsFlushCache").show(); $("#btnShowBackupSettingsModal").show(); $("#btnShowRestoreSettingsModal").show(); } divDnsSettingsLoader.hide(); divDnsSettings.show(); }, error: function () { divDnsSettingsLoader.hide(); divDnsSettings.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDnsSettingsLoader }); } function getArrayAsString(array) { var value = ""; for (var i = 0; i < array.length; i++) value += array[i] + "\r\n"; return value; } function updateDnsSettingsDataAndGui(responseJSON) { sessionData.info.dnsServerDomain = responseJSON.response.dnsServerDomain; sessionData.info.uptimestamp = responseJSON.response.uptimestamp; //update timestamp since server may have restarted during current session document.title = responseJSON.response.dnsServerDomain + " - " + "Technitium DNS Server v" + responseJSON.response.version; $("#lblAboutVersion").text(responseJSON.response.version); $("#lblAboutUptime").text(moment(responseJSON.response.uptimestamp).local().format("lll") + " (" + moment(responseJSON.response.uptimestamp).fromNow() + ")"); $("#lblDnsServerDomain").text(" - " + responseJSON.response.dnsServerDomain); } function loadDnsSettings(responseJSON) { //update cluster nodes sessionData.info.clusterNodes = responseJSON.response.clusterNodes; updateAllClusterNodeDropDowns(); if ($("#optSettingsClusterNode").val() == "cluster") updateClusterNodeDropDown($("#optSettingsClusterNode"), true, "cluster"); else updateClusterNodeDropDown($("#optSettingsClusterNode"), true, responseJSON.response.dnsServerDomain); //general $("#txtDnsServerDomain").val(responseJSON.response.dnsServerDomain); var dnsServerLocalEndPoints = responseJSON.response.dnsServerLocalEndPoints; if (dnsServerLocalEndPoints == null) $("#txtDnsServerLocalEndPoints").val(""); else $("#txtDnsServerLocalEndPoints").val(getArrayAsString(dnsServerLocalEndPoints)); $("#txtDnsServerIPv4SourceAddresses").val(getArrayAsString(responseJSON.response.dnsServerIPv4SourceAddresses)); $("#txtDnsServerIPv6SourceAddresses").val(getArrayAsString(responseJSON.response.dnsServerIPv6SourceAddresses)); $("#txtDefaultRecordTtl").val(responseJSON.response.defaultRecordTtl); $("#txtDefaultNsRecordTtl").val(responseJSON.response.defaultNsRecordTtl); $("#txtDefaultSoaRecordTtl").val(responseJSON.response.defaultSoaRecordTtl); sessionData.info.defaultRecordTtl = responseJSON.response.defaultRecordTtl; sessionData.info.defaultNsRecordTtl = responseJSON.response.defaultNsRecordTtl; sessionData.info.defaultSoaRecordTtl = responseJSON.response.defaultSoaRecordTtl; $("#txtDefaultResponsiblePerson").val(responseJSON.response.defaultResponsiblePerson); $("#chkUseSoaSerialDateScheme").prop("checked", responseJSON.response.useSoaSerialDateScheme); $("#txtMinSoaRefresh").val(responseJSON.response.minSoaRefresh); $("#txtMinSoaRetry").val(responseJSON.response.minSoaRetry); $("#txtZoneTransferAllowedNetworks").val(getArrayAsString(responseJSON.response.zoneTransferAllowedNetworks)); $("#txtNotifyAllowedNetworks").val(getArrayAsString(responseJSON.response.notifyAllowedNetworks)); $("#chkDnsAppsEnableAutomaticUpdate").prop("checked", responseJSON.response.dnsAppsEnableAutomaticUpdate); $("#chkPreferIPv6").prop("checked", responseJSON.response.preferIPv6); $("#chkEnableUdpSocketPool").prop("checked", responseJSON.response.enableUdpSocketPool); $("#txtUdpSocketPoolExcludedPorts").prop("disabled", !responseJSON.response.enableUdpSocketPool); $("#txtUdpSocketPoolExcludedPorts").val(getArrayAsString(responseJSON.response.socketPoolExcludedPorts)); $("#txtEdnsUdpPayloadSize").val(responseJSON.response.udpPayloadSize); $("#chkDnssecValidation").prop("checked", responseJSON.response.dnssecValidation); $("#chkEDnsClientSubnet").prop("checked", responseJSON.response.eDnsClientSubnet); $("#txtEDnsClientSubnetIPv4PrefixLength").prop("disabled", !responseJSON.response.eDnsClientSubnet); $("#txtEDnsClientSubnetIPv6PrefixLength").prop("disabled", !responseJSON.response.eDnsClientSubnet); $("#txtEDnsClientSubnetIpv4Override").prop("disabled", !responseJSON.response.eDnsClientSubnet); $("#txtEDnsClientSubnetIpv6Override").prop("disabled", !responseJSON.response.eDnsClientSubnet); $("#txtEDnsClientSubnetIPv4PrefixLength").val(responseJSON.response.eDnsClientSubnetIPv4PrefixLength); $("#txtEDnsClientSubnetIPv6PrefixLength").val(responseJSON.response.eDnsClientSubnetIPv6PrefixLength); $("#txtEDnsClientSubnetIpv4Override").val(responseJSON.response.eDnsClientSubnetIpv4Override); $("#txtEDnsClientSubnetIpv6Override").val(responseJSON.response.eDnsClientSubnetIpv6Override); $("#tableQpmPrefixLimitsIPv4").html(""); if (responseJSON.response.qpmPrefixLimitsIPv4 != null) { for (var i = 0; i < responseJSON.response.qpmPrefixLimitsIPv4.length; i++) { addQpmPrefixLimitsIPv4Row(responseJSON.response.qpmPrefixLimitsIPv4[i].prefix, responseJSON.response.qpmPrefixLimitsIPv4[i].udpLimit, responseJSON.response.qpmPrefixLimitsIPv4[i].tcpLimit); } } $("#tableQpmPrefixLimitsIPv6").html(""); if (responseJSON.response.qpmPrefixLimitsIPv6 != null) { for (var i = 0; i < responseJSON.response.qpmPrefixLimitsIPv6.length; i++) { addQpmPrefixLimitsIPv6Row(responseJSON.response.qpmPrefixLimitsIPv6[i].prefix, responseJSON.response.qpmPrefixLimitsIPv6[i].udpLimit, responseJSON.response.qpmPrefixLimitsIPv6[i].tcpLimit); } } $("#txtQpmLimitSampleMinutes").val(responseJSON.response.qpmLimitSampleMinutes); $("#txtQpmLimitUdpTruncation").val(responseJSON.response.qpmLimitUdpTruncationPercentage); $("#txtQpmLimitBypassList").val(getArrayAsString(responseJSON.response.qpmLimitBypassList)); $("#txtClientTimeout").val(responseJSON.response.clientTimeout); $("#txtTcpSendTimeout").val(responseJSON.response.tcpSendTimeout); $("#txtTcpReceiveTimeout").val(responseJSON.response.tcpReceiveTimeout); $("#txtQuicIdleTimeout").val(responseJSON.response.quicIdleTimeout); $("#txtQuicMaxInboundStreams").val(responseJSON.response.quicMaxInboundStreams); $("#txtListenBacklog").val(responseJSON.response.listenBacklog); $("#txtMaxConcurrentResolutionsPerCore").val(responseJSON.response.maxConcurrentResolutionsPerCore); //web service var webServiceLocalAddresses = responseJSON.response.webServiceLocalAddresses; if (webServiceLocalAddresses == null) $("#txtWebServiceLocalAddresses").val(""); else $("#txtWebServiceLocalAddresses").val(getArrayAsString(webServiceLocalAddresses)); $("#txtWebServiceHttpPort").val(responseJSON.response.webServiceHttpPort); $("#chkWebServiceEnableTls").prop("checked", responseJSON.response.webServiceEnableTls); $("#chkWebServiceEnableHttp3").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#chkWebServiceHttpToTlsRedirect").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#chkWebServiceUseSelfSignedTlsCertificate").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#txtWebServiceTlsPort").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#txtWebServiceTlsCertificatePath").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#txtWebServiceTlsCertificatePassword").prop("disabled", !responseJSON.response.webServiceEnableTls); $("#chkWebServiceEnableHttp3").prop("checked", responseJSON.response.webServiceEnableHttp3); $("#chkWebServiceHttpToTlsRedirect").prop("checked", responseJSON.response.webServiceHttpToTlsRedirect); $("#chkWebServiceUseSelfSignedTlsCertificate").prop("checked", responseJSON.response.webServiceUseSelfSignedTlsCertificate); $("#txtWebServiceTlsPort").val(responseJSON.response.webServiceTlsPort); $("#txtWebServiceTlsCertificatePath").val(responseJSON.response.webServiceTlsCertificatePath); if (responseJSON.response.webServiceTlsCertificatePath == null) $("#txtWebServiceTlsCertificatePassword").val(""); else $("#txtWebServiceTlsCertificatePassword").val(responseJSON.response.webServiceTlsCertificatePassword); $("#txtWebServiceRealIpHeader").val(responseJSON.response.webServiceRealIpHeader); $("#lblWebServiceRealIpHeader").text(responseJSON.response.webServiceRealIpHeader); $("#lblWebServiceRealIpNginx").text("proxy_set_header " + responseJSON.response.webServiceRealIpHeader + " $remote_addr;"); //optional protocols $("#chkEnableDnsOverUdpProxy").prop("checked", responseJSON.response.enableDnsOverUdpProxy); $("#chkEnableDnsOverTcpProxy").prop("checked", responseJSON.response.enableDnsOverTcpProxy); $("#chkEnableDnsOverHttp").prop("checked", responseJSON.response.enableDnsOverHttp); $("#chkEnableDnsOverTls").prop("checked", responseJSON.response.enableDnsOverTls); $("#chkEnableDnsOverHttps").prop("checked", responseJSON.response.enableDnsOverHttps); $("#chkEnableDnsOverHttp3").prop("disabled", !responseJSON.response.enableDnsOverHttps); $("#chkEnableDnsOverHttp3").prop("checked", responseJSON.response.enableDnsOverHttp3); $("#chkEnableDnsOverQuic").prop("checked", responseJSON.response.enableDnsOverQuic); $("#txtDnsOverUdpProxyPort").prop("disabled", !responseJSON.response.enableDnsOverUdpProxy); $("#txtDnsOverTcpProxyPort").prop("disabled", !responseJSON.response.enableDnsOverTcpProxy); $("#txtDnsOverHttpPort").prop("disabled", !responseJSON.response.enableDnsOverHttp); $("#txtDnsOverTlsPort").prop("disabled", !responseJSON.response.enableDnsOverTls); $("#txtDnsOverHttpsPort").prop("disabled", !responseJSON.response.enableDnsOverHttps); $("#txtDnsOverQuicPort").prop("disabled", !responseJSON.response.enableDnsOverQuic); $("#txtDnsOverUdpProxyPort").val(responseJSON.response.dnsOverUdpProxyPort); $("#txtDnsOverTcpProxyPort").val(responseJSON.response.dnsOverTcpProxyPort); $("#txtDnsOverHttpPort").val(responseJSON.response.dnsOverHttpPort); $("#txtDnsOverTlsPort").val(responseJSON.response.dnsOverTlsPort); $("#txtDnsOverHttpsPort").val(responseJSON.response.dnsOverHttpsPort); $("#txtDnsOverQuicPort").val(responseJSON.response.dnsOverQuicPort); $("#txtReverseProxyNetworkACL").prop("disabled", !responseJSON.response.enableDnsOverUdpProxy && !responseJSON.response.enableDnsOverTcpProxy && !responseJSON.response.enableDnsOverHttp && !responseJSON.response.enableDnsOverHttps); $("#txtReverseProxyNetworkACL").val(getArrayAsString(responseJSON.response.reverseProxyNetworkACL)); $("#txtDnsTlsCertificatePath").prop("disabled", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps && !responseJSON.response.enableDnsOverQuic); $("#txtDnsTlsCertificatePassword").prop("disabled", !responseJSON.response.enableDnsOverTls && !responseJSON.response.enableDnsOverHttps && !responseJSON.response.enableDnsOverQuic); $("#txtDnsTlsCertificatePath").val(responseJSON.response.dnsTlsCertificatePath); if (responseJSON.response.dnsTlsCertificatePath == null) $("#txtDnsTlsCertificatePassword").val(""); else $("#txtDnsTlsCertificatePassword").val(responseJSON.response.dnsTlsCertificatePassword); $("#lblDoHHost").text(window.location.hostname + (responseJSON.response.dnsOverHttpPort == 80 ? "" : ":" + responseJSON.response.dnsOverHttpPort)); $("#lblDoTHost").text("tls-certificate-domain:" + responseJSON.response.dnsOverTlsPort); $("#lblDoQHost").text("tls-certificate-domain:" + responseJSON.response.dnsOverQuicPort); $("#lblDoHsHost").text("tls-certificate-domain" + (responseJSON.response.dnsOverHttpsPort == 443 ? "" : ":" + responseJSON.response.dnsOverHttpsPort)); $("#txtDnsOverHttpRealIpHeader").prop("disabled", !responseJSON.response.enableDnsOverHttp && !responseJSON.response.enableDnsOverHttps); $("#txtDnsOverHttpRealIpHeader").val(responseJSON.response.dnsOverHttpRealIpHeader); $("#lblDnsOverHttpRealIpHeader").text(responseJSON.response.dnsOverHttpRealIpHeader); $("#lblDnsOverHttpRealIpNginx").text("proxy_set_header " + responseJSON.response.dnsOverHttpRealIpHeader + " $remote_addr;"); //tsig $("#tableTsigKeys").html(""); if (responseJSON.response.tsigKeys != null) { for (var i = 0; i < responseJSON.response.tsigKeys.length; i++) { addTsigKeyRow(responseJSON.response.tsigKeys[i].keyName, responseJSON.response.tsigKeys[i].sharedSecret, responseJSON.response.tsigKeys[i].algorithmName); } } //recursion $("#txtRecursionNetworkACL").prop("disabled", true); switch (responseJSON.response.recursion) { case "Allow": $("#rdRecursionAllow").prop("checked", true); break; case "AllowOnlyForPrivateNetworks": $("#rdRecursionAllowOnlyForPrivateNetworks").prop("checked", true); break; case "UseSpecifiedNetworkACL": $("#rdRecursionUseSpecifiedNetworkACL").prop("checked", true); $("#txtRecursionNetworkACL").prop("disabled", false); break; case "Deny": default: $("#rdRecursionDeny").prop("checked", true); break; } $("#txtRecursionNetworkACL").val(getArrayAsString(responseJSON.response.recursionNetworkACL)); $("#chkRandomizeName").prop("checked", responseJSON.response.randomizeName); $("#chkQnameMinimization").prop("checked", responseJSON.response.qnameMinimization); $("#txtResolverRetries").val(responseJSON.response.resolverRetries); $("#txtResolverTimeout").val(responseJSON.response.resolverTimeout); $("#txtResolverConcurrency").val(responseJSON.response.resolverConcurrency); $("#txtResolverMaxStackCount").val(responseJSON.response.resolverMaxStackCount); //cache $("#chkSaveCache").prop("checked", responseJSON.response.saveCache); $("#chkServeStale").prop("checked", responseJSON.response.serveStale); $("#txtServeStaleTtl").prop("disabled", !responseJSON.response.serveStale); $("#txtServeStaleAnswerTtl").prop("disabled", !responseJSON.response.serveStale); $("#txtServeStaleResetTtl").prop("disabled", !responseJSON.response.serveStale); $("#txtServeStaleMaxWaitTime").prop("disabled", !responseJSON.response.serveStale); $("#txtServeStaleTtl").val(responseJSON.response.serveStaleTtl); $("#txtServeStaleAnswerTtl").val(responseJSON.response.serveStaleAnswerTtl); $("#txtServeStaleResetTtl").val(responseJSON.response.serveStaleResetTtl); $("#txtServeStaleMaxWaitTime").val(responseJSON.response.serveStaleMaxWaitTime); $("#txtCacheMaximumEntries").val(responseJSON.response.cacheMaximumEntries); $("#txtCacheMinimumRecordTtl").val(responseJSON.response.cacheMinimumRecordTtl); $("#txtCacheMaximumRecordTtl").val(responseJSON.response.cacheMaximumRecordTtl); $("#txtCacheNegativeRecordTtl").val(responseJSON.response.cacheNegativeRecordTtl); $("#txtCacheFailureRecordTtl").val(responseJSON.response.cacheFailureRecordTtl); $("#txtCachePrefetchEligibility").val(responseJSON.response.cachePrefetchEligibility); $("#txtCachePrefetchTrigger").val(responseJSON.response.cachePrefetchTrigger); $("#txtCachePrefetchSampleIntervalInMinutes").val(responseJSON.response.cachePrefetchSampleIntervalInMinutes); $("#txtCachePrefetchSampleEligibilityHitsPerHour").val(responseJSON.response.cachePrefetchSampleEligibilityHitsPerHour); //blocking $("#chkEnableBlocking").prop("checked", responseJSON.response.enableBlocking); $("#chkAllowTxtBlockingReport").prop("disabled", !responseJSON.response.enableBlocking); $("#txtTemporaryDisableBlockingMinutes").prop("disabled", !responseJSON.response.enableBlocking); $("#btnTemporaryDisableBlockingNow").prop("disabled", !responseJSON.response.enableBlocking); $("#txtBlockingBypassList").prop("disabled", !responseJSON.response.enableBlocking); $("#rdBlockingTypeAnyAddress").prop("disabled", !responseJSON.response.enableBlocking); $("#rdBlockingTypeNxDomain").prop("disabled", !responseJSON.response.enableBlocking); $("#rdBlockingTypeCustomAddress").prop("disabled", !responseJSON.response.enableBlocking); $("#txtBlockListUrls").prop("disabled", !responseJSON.response.enableBlocking); $("#optQuickBlockList").prop("disabled", !responseJSON.response.enableBlocking); $("#txtBlockListUpdateIntervalHours").prop("disabled", !responseJSON.response.enableBlocking); $("#chkAllowTxtBlockingReport").prop("checked", responseJSON.response.allowTxtBlockingReport); if (responseJSON.response.temporaryDisableBlockingTill == null) $("#lblTemporaryDisableBlockingTill").text("Not Set"); else $("#lblTemporaryDisableBlockingTill").text(moment(responseJSON.response.temporaryDisableBlockingTill).local().format("YYYY-MM-DD HH:mm:ss")); $("#txtTemporaryDisableBlockingMinutes").val(""); $("#txtCustomBlockingAddresses").prop("disabled", true); $("#txtBlockingBypassList").val(getArrayAsString(responseJSON.response.blockingBypassList)); switch (responseJSON.response.blockingType) { case "NxDomain": $("#rdBlockingTypeNxDomain").prop("checked", true); break; case "CustomAddress": $("#rdBlockingTypeCustomAddress").prop("checked", true); $("#txtCustomBlockingAddresses").prop("disabled", !responseJSON.response.enableBlocking); break; case "AnyAddress": default: $("#rdBlockingTypeAnyAddress").prop("checked", true); break; } $("#txtCustomBlockingAddresses").val(getArrayAsString(responseJSON.response.customBlockingAddresses)); $("#txtBlockingAnswerTtl").val(responseJSON.response.blockingAnswerTtl); var blockListUrls = responseJSON.response.blockListUrls; if (blockListUrls == null) { $("#txtBlockListUrls").val(""); $("#btnUpdateBlockListsNow").prop("disabled", true); } else { $("#txtBlockListUrls").val(getArrayAsString(blockListUrls)); $("#btnUpdateBlockListsNow").prop("disabled", !responseJSON.response.enableBlocking); } $("#optQuickBlockList").val("blank"); $("#txtBlockListUpdateIntervalHours").val(responseJSON.response.blockListUpdateIntervalHours); if (responseJSON.response.blockListNextUpdatedOn == null) { $("#lblBlockListNextUpdatedOn").text("Not Scheduled"); } else { var blockListNextUpdatedOn = moment(responseJSON.response.blockListNextUpdatedOn); if (moment().utc().isBefore(blockListNextUpdatedOn)) $("#lblBlockListNextUpdatedOn").text(blockListNextUpdatedOn.local().format("YYYY-MM-DD HH:mm:ss")); else $("#lblBlockListNextUpdatedOn").text("Updating Now"); } //proxy & forwarders var proxy = responseJSON.response.proxy; if (proxy === null) { $("#rdProxyTypeNone").prop("checked", true); $("#txtProxyAddress").prop("disabled", true); $("#txtProxyPort").prop("disabled", true); $("#txtProxyUsername").prop("disabled", true); $("#txtProxyPassword").prop("disabled", true); $("#txtProxyBypassList").prop("disabled", true); $("#txtProxyAddress").val(""); $("#txtProxyPort").val(""); $("#txtProxyUsername").val(""); $("#txtProxyPassword").val(""); $("#txtProxyBypassList").val(""); } else { switch (proxy.type.toLowerCase()) { case "http": $("#rdProxyTypeHttp").prop("checked", true); break; case "socks5": $("#rdProxyTypeSocks5").prop("checked", true); break; default: $("#rdProxyTypeNone").prop("checked", true); break; } $("#txtProxyAddress").val(proxy.address); $("#txtProxyPort").val(proxy.port); $("#txtProxyUsername").val(proxy.username); $("#txtProxyPassword").val(proxy.password); $("#txtProxyBypassList").val(getArrayAsString(proxy.bypass)); $("#txtProxyAddress").prop("disabled", false); $("#txtProxyPort").prop("disabled", false); $("#txtProxyUsername").prop("disabled", false); $("#txtProxyPassword").prop("disabled", false); $("#txtProxyBypassList").prop("disabled", false); } var forwarders = responseJSON.response.forwarders; if (forwarders == null) $("#txtForwarders").val(""); else $("#txtForwarders").val(getArrayAsString(forwarders)); $("#optQuickForwarders").val("blank"); switch (responseJSON.response.forwarderProtocol.toLowerCase()) { case "tcp": $("#rdForwarderProtocolTcp").prop("checked", true); break; case "tls": $("#rdForwarderProtocolTls").prop("checked", true); break; case "https": $("#rdForwarderProtocolHttps").prop("checked", true); break; case "quic": $("#rdForwarderProtocolQuic").prop("checked", true); break; default: $("#rdForwarderProtocolUdp").prop("checked", true); break; } $("#chkEnableConcurrentForwarding").prop("checked", responseJSON.response.concurrentForwarding); $("#txtForwarderConcurrency").prop("disabled", !responseJSON.response.concurrentForwarding) $("#txtForwarderRetries").val(responseJSON.response.forwarderRetries); $("#txtForwarderTimeout").val(responseJSON.response.forwarderTimeout); $("#txtForwarderConcurrency").val(responseJSON.response.forwarderConcurrency); //logging var enableLogging; switch (responseJSON.response.loggingType.toLowerCase()) { case "file": $("#rdLoggingTypeFile").prop("checked", true); enableLogging = true; break; case "console": $("#rdLoggingTypeConsole").prop("checked", true); enableLogging = true; break; case "fileandconsole": $("#rdLoggingTypeFileAndConsole").prop("checked", true); enableLogging = true; break; default: $("#rdLoggingTypeNone").prop("checked", true); enableLogging = false; break; } $("#chkIgnoreResolverLogs").prop("disabled", !enableLogging); $("#chkLogQueries").prop("disabled", !enableLogging); $("#chkUseLocalTime").prop("disabled", !enableLogging); $("#txtLogFolderPath").prop("disabled", !enableLogging); $("#chkIgnoreResolverLogs").prop("checked", responseJSON.response.ignoreResolverLogs); $("#chkLogQueries").prop("checked", responseJSON.response.logQueries); $("#chkUseLocalTime").prop("checked", responseJSON.response.useLocalTime); $("#txtLogFolderPath").val(responseJSON.response.logFolder); $("#txtMaxLogFileDays").val(responseJSON.response.maxLogFileDays); $("#chkEnableInMemoryStats").prop("checked", responseJSON.response.enableInMemoryStats); $("#txtMaxStatFileDays").val(responseJSON.response.maxStatFileDays); } function saveDnsSettings(objBtn) { var node = $("#optSettingsClusterNode").val(); var includeClusterParameters = (node == "") || (node == "cluster"); var includeNodeParameters = (node == "") || !includeClusterParameters; var formData = "node=" + encodeURIComponent(node); //general if (includeNodeParameters) { var dnsServerDomain = $("#txtDnsServerDomain").val(); if ((dnsServerDomain === null) || (dnsServerDomain === "")) { showAlert("warning", "Missing!", "Please enter server domain name."); $("#txtDnsServerDomain").trigger("focus"); return; } var dnsServerLocalEndPoints = cleanTextList($("#txtDnsServerLocalEndPoints").val()); if ((dnsServerLocalEndPoints.length === 0) || (dnsServerLocalEndPoints === ",")) dnsServerLocalEndPoints = "0.0.0.0:53,[::]:53"; else $("#txtDnsServerLocalEndPoints").val(dnsServerLocalEndPoints.replace(/,/g, "\n")); var dnsServerIPv4SourceAddresses = cleanTextList($("#txtDnsServerIPv4SourceAddresses").val()); if ((dnsServerIPv4SourceAddresses.length == 0) || (dnsServerIPv4SourceAddresses === ",")) dnsServerIPv4SourceAddresses = false; var dnsServerIPv6SourceAddresses = cleanTextList($("#txtDnsServerIPv6SourceAddresses").val()); if ((dnsServerIPv6SourceAddresses.length == 0) || (dnsServerIPv6SourceAddresses === ",")) dnsServerIPv6SourceAddresses = false; formData += "&dnsServerDomain=" + dnsServerDomain + "&dnsServerLocalEndPoints=" + encodeURIComponent(dnsServerLocalEndPoints) + "&dnsServerIPv4SourceAddresses=" + encodeURIComponent(dnsServerIPv4SourceAddresses) + "&dnsServerIPv6SourceAddresses=" + encodeURIComponent(dnsServerIPv6SourceAddresses) } if (includeClusterParameters) { var defaultRecordTtl = $("#txtDefaultRecordTtl").val(); var defaultNsRecordTtl = $("#txtDefaultNsRecordTtl").val(); var defaultSoaRecordTtl = $("#txtDefaultSoaRecordTtl").val(); var defaultResponsiblePerson = $("#txtDefaultResponsiblePerson").val(); var useSoaSerialDateScheme = $("#chkUseSoaSerialDateScheme").prop("checked"); var minSoaRefresh = $("#txtMinSoaRefresh").val(); var minSoaRetry = $("#txtMinSoaRetry").val(); var zoneTransferAllowedNetworks = cleanTextList($("#txtZoneTransferAllowedNetworks").val()); if ((zoneTransferAllowedNetworks.length == 0) || (zoneTransferAllowedNetworks === ",")) zoneTransferAllowedNetworks = false; else $("#txtZoneTransferAllowedNetworks").val(zoneTransferAllowedNetworks.replace(/,/g, "\n") + "\n"); var notifyAllowedNetworks = cleanTextList($("#txtNotifyAllowedNetworks").val()); if ((notifyAllowedNetworks.length == 0) || (notifyAllowedNetworks === ",")) notifyAllowedNetworks = false; else $("#txtNotifyAllowedNetworks").val(notifyAllowedNetworks.replace(/,/g, "\n") + "\n"); var dnsAppsEnableAutomaticUpdate = $("#chkDnsAppsEnableAutomaticUpdate").prop("checked"); formData += "&defaultRecordTtl=" + encodeURIComponent(defaultRecordTtl) + "&defaultNsRecordTtl=" + encodeURIComponent(defaultNsRecordTtl) + "&defaultSoaRecordTtl=" + encodeURIComponent(defaultSoaRecordTtl) + "&defaultResponsiblePerson=" + encodeURIComponent(defaultResponsiblePerson) + "&useSoaSerialDateScheme=" + useSoaSerialDateScheme + "&minSoaRefresh=" + encodeURIComponent(minSoaRefresh) + "&minSoaRetry=" + encodeURIComponent(minSoaRetry) + "&zoneTransferAllowedNetworks=" + encodeURIComponent(zoneTransferAllowedNetworks) + "¬ifyAllowedNetworks=" + encodeURIComponent(notifyAllowedNetworks) + "&dnsAppsEnableAutomaticUpdate=" + dnsAppsEnableAutomaticUpdate; } if (includeNodeParameters) { var preferIPv6 = $("#chkPreferIPv6").prop("checked"); var enableUdpSocketPool = $("#chkEnableUdpSocketPool").prop("checked"); var socketPoolExcludedPorts = cleanTextList($("#txtUdpSocketPoolExcludedPorts").val()); if ((socketPoolExcludedPorts.length == 0) || (socketPoolExcludedPorts === ",")) socketPoolExcludedPorts = false; else $("#txtUdpSocketPoolExcludedPorts").val(socketPoolExcludedPorts.replace(/,/g, "\n") + "\n"); formData += "&preferIPv6=" + preferIPv6 + "&enableUdpSocketPool=" + enableUdpSocketPool + "&socketPoolExcludedPorts=" + encodeURIComponent(socketPoolExcludedPorts); } if (includeClusterParameters) { var udpPayloadSize = $("#txtEdnsUdpPayloadSize").val(); var dnssecValidation = $("#chkDnssecValidation").prop("checked"); var eDnsClientSubnet = $("#chkEDnsClientSubnet").prop("checked"); var eDnsClientSubnetIPv4PrefixLength = $("#txtEDnsClientSubnetIPv4PrefixLength").val(); if ((eDnsClientSubnetIPv4PrefixLength == null) || (eDnsClientSubnetIPv4PrefixLength === "")) { showAlert("warning", "Missing!", "Please enter EDNS Client Subnet IPv4 prefix length."); $("#txtEDnsClientSubnetIPv4PrefixLength").trigger("focus"); return; } var eDnsClientSubnetIPv6PrefixLength = $("#txtEDnsClientSubnetIPv6PrefixLength").val(); if ((eDnsClientSubnetIPv6PrefixLength == null) || (eDnsClientSubnetIPv6PrefixLength === "")) { showAlert("warning", "Missing!", "Please enter EDNS Client Subnet IPv6 prefix length."); $("#txtEDnsClientSubnetIPv6PrefixLength").trigger("focus"); return; } var eDnsClientSubnetIpv4Override = $("#txtEDnsClientSubnetIpv4Override").val(); var eDnsClientSubnetIpv6Override = $("#txtEDnsClientSubnetIpv6Override").val(); var qpmPrefixLimitsIPv4 = serializeTableData($("#tableQpmPrefixLimitsIPv4"), 3); if (qpmPrefixLimitsIPv4 === false) return; if (qpmPrefixLimitsIPv4.length === 0) qpmPrefixLimitsIPv4 = false; var qpmPrefixLimitsIPv6 = serializeTableData($("#tableQpmPrefixLimitsIPv6"), 3); if (qpmPrefixLimitsIPv6 === false) return; if (qpmPrefixLimitsIPv6.length === 0) qpmPrefixLimitsIPv6 = false; var qpmLimitSampleMinutes = $("#txtQpmLimitSampleMinutes").val(); if ((qpmLimitSampleMinutes == null) || (qpmLimitSampleMinutes === "")) { showAlert("warning", "Missing!", "Please enter Queries Per Minute (QPM) sample value."); $("#txtQpmLimitSampleMinutes").trigger("focus"); return; } var qpmLimitUdpTruncationPercentage = $("#txtQpmLimitUdpTruncation").val(); if ((qpmLimitUdpTruncationPercentage == null) || (qpmLimitUdpTruncationPercentage === "")) { showAlert("warning", "Missing!", "Please enter Queries Per Minute (QPM) limit UDP truncation percentage value."); $("#txtQpmLimitUdpTruncation").trigger("focus"); return; } var qpmLimitBypassList = cleanTextList($("#txtQpmLimitBypassList").val()); if ((qpmLimitBypassList.length == 0) || (qpmLimitBypassList === ",")) qpmLimitBypassList = false; else $("#txtQpmLimitBypassList").val(qpmLimitBypassList.replace(/,/g, "\n") + "\n"); var clientTimeout = $("#txtClientTimeout").val(); if ((clientTimeout == null) || (clientTimeout === "")) { showAlert("warning", "Missing!", "Please enter a value for Client Timeout."); $("#txtClientTimeout").trigger("focus"); return; } var tcpSendTimeout = $("#txtTcpSendTimeout").val(); if ((tcpSendTimeout == null) || (tcpSendTimeout === "")) { showAlert("warning", "Missing!", "Please enter a value for TCP Send Timeout."); $("#txtTcpSendTimeout").trigger("focus"); return; } var tcpReceiveTimeout = $("#txtTcpReceiveTimeout").val(); if ((tcpReceiveTimeout == null) || (tcpReceiveTimeout === "")) { showAlert("warning", "Missing!", "Please enter a value for TCP Receive Timeout."); $("#txtTcpReceiveTimeout").trigger("focus"); return; } var quicIdleTimeout = $("#txtQuicIdleTimeout").val(); if ((quicIdleTimeout == null) || (quicIdleTimeout === "")) { showAlert("warning", "Missing!", "Please enter a value for QUIC Idle Timeout."); $("#txtQuicIdleTimeout").trigger("focus"); return; } var quicMaxInboundStreams = $("#txtQuicMaxInboundStreams").val(); if ((quicMaxInboundStreams == null) || (quicMaxInboundStreams === "")) { showAlert("warning", "Missing!", "Please enter a value for QUIC Max Inbound Streams."); $("#txtQuicMaxInboundStreams").trigger("focus"); return; } var listenBacklog = $("#txtListenBacklog").val(); if ((listenBacklog == null) || (listenBacklog === "")) { showAlert("warning", "Missing!", "Please enter a value for Listen Backlog."); $("#txtListenBacklog").trigger("focus"); return; } var maxConcurrentResolutionsPerCore = $("#txtMaxConcurrentResolutionsPerCore").val(); if ((maxConcurrentResolutionsPerCore == null) || (maxConcurrentResolutionsPerCore === "")) { showAlert("warning", "Missing!", "Please enter a value for Max Concurrent Resolutions."); $("#txtMaxConcurrentResolutionsPerCore").trigger("focus"); return; } formData += "&udpPayloadSize=" + udpPayloadSize + "&dnssecValidation=" + dnssecValidation; formData += "&eDnsClientSubnet=" + eDnsClientSubnet + "&eDnsClientSubnetIPv4PrefixLength=" + eDnsClientSubnetIPv4PrefixLength + "&eDnsClientSubnetIPv6PrefixLength=" + eDnsClientSubnetIPv6PrefixLength + "&eDnsClientSubnetIpv4Override=" + encodeURIComponent(eDnsClientSubnetIpv4Override) + "&eDnsClientSubnetIpv6Override=" + encodeURIComponent(eDnsClientSubnetIpv6Override); formData += "&qpmPrefixLimitsIPv4=" + encodeURIComponent(qpmPrefixLimitsIPv4) + "&qpmPrefixLimitsIPv6=" + encodeURIComponent(qpmPrefixLimitsIPv6) + "&qpmLimitSampleMinutes=" + qpmLimitSampleMinutes + "&qpmLimitUdpTruncationPercentage=" + qpmLimitUdpTruncationPercentage + "&qpmLimitBypassList=" + encodeURIComponent(qpmLimitBypassList); formData += "&clientTimeout=" + clientTimeout + "&tcpSendTimeout=" + tcpSendTimeout + "&tcpReceiveTimeout=" + tcpReceiveTimeout + "&quicIdleTimeout=" + quicIdleTimeout + "&quicMaxInboundStreams=" + quicMaxInboundStreams + "&listenBacklog=" + listenBacklog + "&maxConcurrentResolutionsPerCore=" + maxConcurrentResolutionsPerCore; } //web service if (includeNodeParameters) { var webServiceLocalAddresses = cleanTextList($("#txtWebServiceLocalAddresses").val()); if ((webServiceLocalAddresses.length === 0) || (webServiceLocalAddresses === ",")) webServiceLocalAddresses = "0.0.0.0,[::]"; else $("#txtWebServiceLocalAddresses").val(webServiceLocalAddresses.replace(/,/g, "\n")); var webServiceHttpPort = $("#txtWebServiceHttpPort").val(); if ((webServiceHttpPort === null) || (webServiceHttpPort === "")) webServiceHttpPort = 5380; var webServiceEnableTls = $("#chkWebServiceEnableTls").prop("checked"); var webServiceEnableHttp3 = $("#chkWebServiceEnableHttp3").prop("checked"); var webServiceHttpToTlsRedirect = $("#chkWebServiceHttpToTlsRedirect").prop("checked"); var webServiceUseSelfSignedTlsCertificate = $("#chkWebServiceUseSelfSignedTlsCertificate").prop("checked"); var webServiceTlsPort = $("#txtWebServiceTlsPort").val(); var webServiceTlsCertificatePath = $("#txtWebServiceTlsCertificatePath").val(); var webServiceTlsCertificatePassword = $("#txtWebServiceTlsCertificatePassword").val(); var webServiceRealIpHeader = $("#txtWebServiceRealIpHeader").val(); formData += "&webServiceLocalAddresses=" + encodeURIComponent(webServiceLocalAddresses) + "&webServiceHttpPort=" + webServiceHttpPort + "&webServiceEnableTls=" + webServiceEnableTls + "&webServiceEnableHttp3=" + webServiceEnableHttp3 + "&webServiceHttpToTlsRedirect=" + webServiceHttpToTlsRedirect + "&webServiceUseSelfSignedTlsCertificate=" + webServiceUseSelfSignedTlsCertificate + "&webServiceTlsPort=" + webServiceTlsPort + "&webServiceTlsCertificatePath=" + encodeURIComponent(webServiceTlsCertificatePath) + "&webServiceTlsCertificatePassword=" + encodeURIComponent(webServiceTlsCertificatePassword) + "&webServiceRealIpHeader=" + encodeURIComponent(webServiceRealIpHeader); } //optional protocols if (includeNodeParameters) { var enableDnsOverUdpProxy = $("#chkEnableDnsOverUdpProxy").prop("checked"); var enableDnsOverTcpProxy = $("#chkEnableDnsOverTcpProxy").prop("checked"); var enableDnsOverHttp = $("#chkEnableDnsOverHttp").prop("checked"); var enableDnsOverTls = $("#chkEnableDnsOverTls").prop("checked"); var enableDnsOverHttps = $("#chkEnableDnsOverHttps").prop("checked"); var enableDnsOverHttp3 = $("#chkEnableDnsOverHttp3").prop("checked"); var enableDnsOverQuic = $("#chkEnableDnsOverQuic").prop("checked"); var dnsOverUdpProxyPort = $("#txtDnsOverUdpProxyPort").val(); if ((dnsOverUdpProxyPort == null) || (dnsOverUdpProxyPort === "")) { showAlert("warning", "Missing!", "Please enter a value for DNS-over-UDP-PROXY Port."); $("#txtDnsOverUdpProxyPort").trigger("focus"); return; } var dnsOverTcpProxyPort = $("#txtDnsOverTcpProxyPort").val(); if ((dnsOverTcpProxyPort == null) || (dnsOverTcpProxyPort === "")) { showAlert("warning", "Missing!", "Please enter a value for DNS-over-TCP-PROXY Port."); $("#txtDnsOverTcpProxyPort").trigger("focus"); return; } var dnsOverHttpPort = $("#txtDnsOverHttpPort").val(); if ((dnsOverHttpPort == null) || (dnsOverHttpPort === "")) { showAlert("warning", "Missing!", "Please enter a value for DNS-over-HTTP Port."); $("#txtDnsOverHttpPort").trigger("focus"); return; } var dnsOverTlsPort = $("#txtDnsOverTlsPort").val(); if ((dnsOverTlsPort == null) || (dnsOverTlsPort === "")) { showAlert("warning", "Missing!", "Please enter a value for DNS-over-TLS Port."); $("#txtDnsOverTlsPort").trigger("focus"); return; } var dnsOverHttpsPort = $("#txtDnsOverHttpsPort").val(); if ((dnsOverHttpsPort == null) || (dnsOverHttpsPort === "")) { showAlert("warning", "Missing!", "Please enter a value for DNS-over-HTTPS Port."); $("#txtDnsOverHttpsPort").trigger("focus"); return; } var dnsOverQuicPort = $("#txtDnsOverQuicPort").val(); if ((dnsOverQuicPort == null) || (dnsOverQuicPort === "")) { showAlert("warning", "Missing!", "Please enter a value for DNS-over-QUIC Port."); $("#txtDnsOverQuicPort").trigger("focus"); return; } var reverseProxyNetworkACL = cleanTextList($("#txtReverseProxyNetworkACL").val()); if ((reverseProxyNetworkACL.length === 0) || (reverseProxyNetworkACL === ",")) reverseProxyNetworkACL = false; else $("#txtReverseProxyNetworkACL").val(reverseProxyNetworkACL.replace(/,/g, "\n")); var dnsTlsCertificatePath = $("#txtDnsTlsCertificatePath").val(); var dnsTlsCertificatePassword = $("#txtDnsTlsCertificatePassword").val(); var dnsOverHttpRealIpHeader = $("#txtDnsOverHttpRealIpHeader").val(); formData += "&enableDnsOverUdpProxy=" + enableDnsOverUdpProxy + "&enableDnsOverTcpProxy=" + enableDnsOverTcpProxy + "&enableDnsOverHttp=" + enableDnsOverHttp + "&enableDnsOverTls=" + enableDnsOverTls + "&enableDnsOverHttps=" + enableDnsOverHttps + "&enableDnsOverHttp3=" + enableDnsOverHttp3 + "&enableDnsOverQuic=" + enableDnsOverQuic + "&dnsOverUdpProxyPort=" + dnsOverUdpProxyPort + "&dnsOverTcpProxyPort=" + dnsOverTcpProxyPort + "&dnsOverHttpPort=" + dnsOverHttpPort + "&dnsOverTlsPort=" + dnsOverTlsPort + "&dnsOverHttpsPort=" + dnsOverHttpsPort + "&dnsOverQuicPort=" + dnsOverQuicPort + "&reverseProxyNetworkACL=" + encodeURIComponent(reverseProxyNetworkACL) + "&dnsTlsCertificatePath=" + encodeURIComponent(dnsTlsCertificatePath) + "&dnsTlsCertificatePassword=" + encodeURIComponent(dnsTlsCertificatePassword) + "&dnsOverHttpRealIpHeader=" + encodeURIComponent(dnsOverHttpRealIpHeader); } //tsig if (includeClusterParameters) { var tsigKeys = serializeTableData($("#tableTsigKeys"), 3); if (tsigKeys === false) return; if (tsigKeys.length === 0) tsigKeys = false; formData += "&tsigKeys=" + encodeURIComponent(tsigKeys); } //recursion if (includeClusterParameters) { var recursion = $("input[name=rdRecursion]:checked").val(); var recursionNetworkACL = cleanTextList($("#txtRecursionNetworkACL").val()); if ((recursionNetworkACL.length === 0) || (recursionNetworkACL === ",")) recursionNetworkACL = false; else $("#txtRecursionNetworkACL").val(recursionNetworkACL.replace(/,/g, "\n")); var randomizeName = $("#chkRandomizeName").prop("checked"); var qnameMinimization = $("#chkQnameMinimization").prop("checked"); var resolverRetries = $("#txtResolverRetries").val(); if ((resolverRetries == null) || (resolverRetries === "")) { showAlert("warning", "Missing!", "Please enter a value for Resolver Retries."); $("#txtResolverRetries").trigger("focus"); return; } var resolverTimeout = $("#txtResolverTimeout").val(); if ((resolverTimeout == null) || (resolverTimeout === "")) { showAlert("warning", "Missing!", "Please enter a value for Resolver Timeout."); $("#txtResolverTimeout").trigger("focus"); return; } var resolverConcurrency = $("#txtResolverConcurrency").val(); if ((resolverConcurrency == null) || (resolverConcurrency === "")) { showAlert("warning", "Missing!", "Please enter a value for Resolver Concurrency."); $("#txtResolverConcurrency").trigger("focus"); return; } var resolverMaxStackCount = $("#txtResolverMaxStackCount").val(); if ((resolverMaxStackCount == null) || (resolverMaxStackCount === "")) { showAlert("warning", "Missing!", "Please enter a value for Resolver Max Stack Count."); $("#txtResolverMaxStackCount").trigger("focus"); return; } formData += "&recursion=" + recursion + "&recursionNetworkACL=" + encodeURIComponent(recursionNetworkACL) + "&randomizeName=" + randomizeName + "&qnameMinimization=" + qnameMinimization + "&resolverRetries=" + resolverRetries + "&resolverTimeout=" + resolverTimeout + "&resolverConcurrency=" + resolverConcurrency + "&resolverMaxStackCount=" + resolverMaxStackCount; } //cache if (includeNodeParameters) { var saveCache = $("#chkSaveCache").prop("checked"); var serveStale = $("#chkServeStale").prop("checked"); var serveStaleTtl = $("#txtServeStaleTtl").val(); var serveStaleAnswerTtl = $("#txtServeStaleAnswerTtl").val(); var serveStaleResetTtl = $("#txtServeStaleResetTtl").val(); var serveStaleMaxWaitTime = $("#txtServeStaleMaxWaitTime").val(); var cacheMaximumEntries = $("#txtCacheMaximumEntries").val(); if ((cacheMaximumEntries === null) || (cacheMaximumEntries === "")) { showAlert("warning", "Missing!", "Please enter cache maximum entries value."); $("#txtCacheMaximumEntries").trigger("focus"); return; } var cacheMinimumRecordTtl = $("#txtCacheMinimumRecordTtl").val(); if ((cacheMinimumRecordTtl === null) || (cacheMinimumRecordTtl === "")) { showAlert("warning", "Missing!", "Please enter cache minimum record TTL value."); $("#txtCacheMinimumRecordTtl").trigger("focus"); return; } var cacheMaximumRecordTtl = $("#txtCacheMaximumRecordTtl").val(); if ((cacheMaximumRecordTtl === null) || (cacheMaximumRecordTtl === "")) { showAlert("warning", "Missing!", "Please enter cache maximum record TTL value."); $("#txtCacheMaximumRecordTtl").trigger("focus"); return; } var cacheNegativeRecordTtl = $("#txtCacheNegativeRecordTtl").val(); if ((cacheNegativeRecordTtl === null) || (cacheNegativeRecordTtl === "")) { showAlert("warning", "Missing!", "Please enter cache negative record TTL value."); $("#txtCacheNegativeRecordTtl").trigger("focus"); return; } var cacheFailureRecordTtl = $("#txtCacheFailureRecordTtl").val(); if ((cacheFailureRecordTtl === null) || (cacheFailureRecordTtl === "")) { showAlert("warning", "Missing!", "Please enter cache failure record TTL value."); $("#txtCacheFailureRecordTtl").trigger("focus"); return; } var cachePrefetchEligibility = $("#txtCachePrefetchEligibility").val(); if ((cachePrefetchEligibility === null) || (cachePrefetchEligibility === "")) { showAlert("warning", "Missing!", "Please enter cache prefetch eligibility value."); $("#txtCachePrefetchEligibility").trigger("focus"); return; } var cachePrefetchTrigger = $("#txtCachePrefetchTrigger").val(); if ((cachePrefetchTrigger === null) || (cachePrefetchTrigger === "")) { showAlert("warning", "Missing!", "Please enter cache prefetch trigger value."); $("#txtCachePrefetchTrigger").trigger("focus"); return; } var cachePrefetchSampleIntervalInMinutes = $("#txtCachePrefetchSampleIntervalInMinutes").val(); if ((cachePrefetchSampleIntervalInMinutes === null) || (cachePrefetchSampleIntervalInMinutes === "")) { showAlert("warning", "Missing!", "Please enter cache auto prefetch sample interval value."); $("#txtCachePrefetchSampleIntervalInMinutes").trigger("focus"); return; } var cachePrefetchSampleEligibilityHitsPerHour = $("#txtCachePrefetchSampleEligibilityHitsPerHour").val(); if ((cachePrefetchSampleEligibilityHitsPerHour === null) || (cachePrefetchSampleEligibilityHitsPerHour === "")) { showAlert("warning", "Missing!", "Please enter cache auto prefetch sample eligibility value."); $("#txtCachePrefetchSampleEligibilityHitsPerHour").trigger("focus"); return; } formData += "&saveCache=" + saveCache + "&serveStale=" + serveStale + "&serveStaleTtl=" + serveStaleTtl + "&serveStaleAnswerTtl=" + serveStaleAnswerTtl + "&serveStaleResetTtl=" + serveStaleResetTtl + "&serveStaleMaxWaitTime=" + serveStaleMaxWaitTime + "&cacheMaximumEntries=" + cacheMaximumEntries + "&cacheMinimumRecordTtl=" + cacheMinimumRecordTtl + "&cacheMaximumRecordTtl=" + cacheMaximumRecordTtl + "&cacheNegativeRecordTtl=" + cacheNegativeRecordTtl + "&cacheFailureRecordTtl=" + cacheFailureRecordTtl + "&cachePrefetchEligibility=" + cachePrefetchEligibility + "&cachePrefetchTrigger=" + cachePrefetchTrigger + "&cachePrefetchSampleIntervalInMinutes=" + cachePrefetchSampleIntervalInMinutes + "&cachePrefetchSampleEligibilityHitsPerHour=" + cachePrefetchSampleEligibilityHitsPerHour; } //blocking if (includeClusterParameters) { var enableBlocking = $("#chkEnableBlocking").prop("checked"); var allowTxtBlockingReport = $("#chkAllowTxtBlockingReport").prop("checked"); var blockingBypassList = cleanTextList($("#txtBlockingBypassList").val()); if ((blockingBypassList.length == 0) || (blockingBypassList === ",")) blockingBypassList = false; else $("#txtBlockingBypassList").val(blockingBypassList.replace(/,/g, "\n") + "\n"); var blockingType = $("input[name=rdBlockingType]:checked").val(); var customBlockingAddresses = cleanTextList($("#txtCustomBlockingAddresses").val()); if ((customBlockingAddresses.length === 0) || customBlockingAddresses === ",") customBlockingAddresses = false; else $("#txtCustomBlockingAddresses").val(customBlockingAddresses.replace(/,/g, "\n") + "\n"); var blockingAnswerTtl = $("#txtBlockingAnswerTtl").val(); var blockListUrls = cleanTextList($("#txtBlockListUrls").val()); if ((blockListUrls.length === 0) || (blockListUrls === ",")) blockListUrls = false; else $("#txtBlockListUrls").val(blockListUrls.replace(/,/g, "\n") + "\n"); var blockListUpdateIntervalHours = $("#txtBlockListUpdateIntervalHours").val(); formData += "&enableBlocking=" + enableBlocking + "&allowTxtBlockingReport=" + allowTxtBlockingReport + "&blockingBypassList=" + encodeURIComponent(blockingBypassList) + "&blockingType=" + blockingType + "&customBlockingAddresses=" + encodeURIComponent(customBlockingAddresses) + "&blockingAnswerTtl=" + blockingAnswerTtl + "&blockListUrls=" + encodeURIComponent(blockListUrls) + "&blockListUpdateIntervalHours=" + blockListUpdateIntervalHours; } //proxy & forwarders if (includeClusterParameters) { var proxy; var proxyType = $("input[name=rdProxyType]:checked").val().toLowerCase(); if (proxyType === "none") { proxy = "&proxyType=" + proxyType; } else { var proxyAddress = $("#txtProxyAddress").val(); if ((proxyAddress === null) || (proxyAddress === "")) { showAlert("warning", "Missing!", "Please enter proxy server address."); $("#txtProxyAddress").trigger("focus"); return; } var proxyPort = $("#txtProxyPort").val(); if ((proxyPort === null) || (proxyPort === "")) { showAlert("warning", "Missing!", "Please enter proxy server port."); $("#txtProxyPort").trigger("focus"); return; } var proxyBypass = cleanTextList($("#txtProxyBypassList").val()); if ((proxyBypass.length === 0) || (proxyBypass === ",")) proxyBypass = ""; else $("#txtProxyBypassList").val(proxyBypass.replace(/,/g, "\n")); proxy = "&proxyType=" + proxyType + "&proxyAddress=" + encodeURIComponent(proxyAddress) + "&proxyPort=" + proxyPort + "&proxyUsername=" + encodeURIComponent($("#txtProxyUsername").val()) + "&proxyPassword=" + encodeURIComponent($("#txtProxyPassword").val()) + "&proxyBypass=" + encodeURIComponent(proxyBypass); } var forwarders = cleanTextList($("#txtForwarders").val()); if ((forwarders.length === 0) || (forwarders === ",")) forwarders = false; else $("#txtForwarders").val(forwarders.replace(/,/g, "\n")); var forwarderProtocol = $("input[name=rdForwarderProtocol]:checked").val(); var concurrentForwarding = $("#chkEnableConcurrentForwarding").prop("checked"); var forwarderRetries = $("#txtForwarderRetries").val(); if ((forwarderRetries == null) || (forwarderRetries === "")) { showAlert("warning", "Missing!", "Please enter a value for Forwarder Retries."); $("#txtForwarderRetries").trigger("focus"); return; } var forwarderTimeout = $("#txtForwarderTimeout").val(); if ((forwarderTimeout == null) || (forwarderTimeout === "")) { showAlert("warning", "Missing!", "Please enter a value for Forwarder Timeout."); $("#txtForwarderTimeout").trigger("focus"); return; } var forwarderConcurrency = $("#txtForwarderConcurrency").val(); if ((forwarderConcurrency == null) || (forwarderConcurrency === "")) { showAlert("warning", "Missing!", "Please enter a value for Forwarder Concurrency."); $("#txtForwarderConcurrency").trigger("focus"); return; } formData += proxy + "&forwarders=" + encodeURIComponent(forwarders) + "&forwarderProtocol=" + forwarderProtocol + "&concurrentForwarding=" + concurrentForwarding + "&forwarderRetries=" + forwarderRetries + "&forwarderTimeout=" + forwarderTimeout + "&forwarderConcurrency=" + forwarderConcurrency; } //logging if (includeNodeParameters) { var loggingType = $("input[name=rdLoggingType]:checked").val(); var ignoreResolverLogs = $("#chkIgnoreResolverLogs").prop("checked"); var logQueries = $("#chkLogQueries").prop("checked"); var useLocalTime = $("#chkUseLocalTime").prop("checked"); var logFolder = $("#txtLogFolderPath").val(); var maxLogFileDays = $("#txtMaxLogFileDays").val(); var enableInMemoryStats = $("#chkEnableInMemoryStats").prop("checked"); var maxStatFileDays = $("#txtMaxStatFileDays").val(); formData += "&loggingType=" + loggingType + "&ignoreResolverLogs=" + ignoreResolverLogs + "&logQueries=" + logQueries + "&useLocalTime=" + useLocalTime + "&logFolder=" + encodeURIComponent(logFolder) + "&maxLogFileDays=" + maxLogFileDays + "&enableInMemoryStats=" + enableInMemoryStats + "&maxStatFileDays=" + maxStatFileDays; } //send request var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/settings/set?token=" + sessionData.token, method: "POST", data: formData, processData: false, showInnerError: true, success: function (responseJSON) { if ((node == "") || (node == sessionData.info.dnsServerDomain)) updateDnsSettingsDataAndGui(responseJSON); loadDnsSettings(responseJSON); btn.button("reset"); showAlert("success", "Settings Saved!", "DNS Server settings were saved successfully."); if (sessionData.info.dnsServerDomain == responseJSON.server) checkForWebConsoleRedirection(responseJSON); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function addQpmPrefixLimitsIPv4Row(prefix, udpLimit, tcpLimit) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableQpmPrefixLimitsIPv4").append(tableHtmlRows); } function addQpmPrefixLimitsIPv6Row(prefix, udpLimit, tcpLimit) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableQpmPrefixLimitsIPv6").append(tableHtmlRows); } function addTsigKeyRow(keyName, sharedSecret, algorithmName) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; $("#tableTsigKeys").append(tableHtmlRows); } function checkForReverseProxy(responseJSON) { if (window.location.protocol == "https:") { var currentPort = window.location.port; if ((currentPort == 0) || (currentPort == "")) currentPort = 443; reverseProxyDetected = !responseJSON.response.webServiceEnableTls || (currentPort != responseJSON.response.webServiceTlsPort); } else { var currentPort = window.location.port; if ((currentPort == 0) || (currentPort == "")) currentPort = 80; reverseProxyDetected = currentPort != responseJSON.response.webServiceHttpPort } } function checkForWebConsoleRedirection(responseJSON) { if (reverseProxyDetected) return; if (location.protocol == "https:") { if (!responseJSON.response.webServiceEnableTls) { setTimeout(function () { window.open("http://" + window.location.hostname + ":" + responseJSON.response.webServiceHttpPort, "_self"); }, 2500); //delay redirection to allow web server to restart return; } var currentPort = window.location.port; if ((currentPort == 0) || (currentPort == "")) currentPort = 443; if (currentPort != responseJSON.response.webServiceTlsPort) { setTimeout(function () { window.open("https://" + window.location.hostname + ":" + responseJSON.response.webServiceTlsPort, "_self"); }, 2500); //delay redirection to allow web server to restart } } else { if (responseJSON.response.webServiceEnableTls && responseJSON.response.webServiceHttpToTlsRedirect) { setTimeout(function () { window.open("https://" + window.location.hostname + ":" + responseJSON.response.webServiceTlsPort, "_self"); }, 2500); //delay redirection to allow web server to restart return; } var currentPort = window.location.port; if ((currentPort == 0) || (currentPort == "")) currentPort = 80; if (currentPort != responseJSON.response.webServiceHttpPort) { setTimeout(function () { window.open("http://" + window.location.hostname + ":" + responseJSON.response.webServiceHttpPort, "_self"); }, 2500); //delay redirection to allow web server to restart } } } function forceUpdateBlockLists() { if (!confirm("Are you sure to force download and update the block lists?")) return; var btn = $("#btnUpdateBlockListsNow"); btn.button("loading"); HTTPRequest({ url: "api/settings/forceUpdateBlockLists?token=" + sessionData.token, success: function (responseJSON) { btn.button("reset"); $("#lblBlockListNextUpdatedOn").text("Updating Now"); showAlert("success", "Updating Block List!", "Block list update was triggered successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function temporaryDisableBlockingNow() { var minutes = $("#txtTemporaryDisableBlockingMinutes").val(); if ((minutes === null) || (minutes === "")) { showAlert("warning", "Missing!", "Please enter a value in minutes to temporarily disable blocking."); $("#txtTemporaryDisableBlockingMinutes").trigger("focus"); return; } if (!confirm("Are you sure to temporarily disable blocking for " + minutes + " minute(s)?")) return; var btn = $("#btnTemporaryDisableBlockingNow"); btn.button("loading"); HTTPRequest({ url: "api/settings/temporaryDisableBlocking?token=" + sessionData.token + "&minutes=" + minutes, success: function (responseJSON) { btn.button("reset"); $("#chkEnableBlocking").prop("checked", false); $("#lblTemporaryDisableBlockingTill").text(moment(responseJSON.response.temporaryDisableBlockingTill).local().format("YYYY-MM-DD HH:mm:ss")); updateBlockingState(); showAlert("success", "Blocking Disabled!", "Blocking was successfully disabled temporarily for " + htmlEncode(minutes) + " minute(s)."); setTimeout(updateBlockingState, 500); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function updateBlockingState() { var enableBlocking = $("#chkEnableBlocking").prop("checked"); $("#chkAllowTxtBlockingReport").prop("disabled", !enableBlocking); $("#txtTemporaryDisableBlockingMinutes").prop("disabled", !enableBlocking); $("#btnTemporaryDisableBlockingNow").prop("disabled", !enableBlocking); $("#txtBlockingBypassList").prop("disabled", !enableBlocking); $("#rdBlockingTypeAnyAddress").prop("disabled", !enableBlocking); $("#rdBlockingTypeNxDomain").prop("disabled", !enableBlocking); $("#rdBlockingTypeCustomAddress").prop("disabled", !enableBlocking); $("#txtCustomBlockingAddresses").prop("disabled", !enableBlocking || !$("#rdBlockingTypeCustomAddress").prop("checked")); $("#txtBlockListUrls").prop("disabled", !enableBlocking); $("#optQuickBlockList").prop("disabled", !enableBlocking); $("#txtBlockListUpdateIntervalHours").prop("disabled", !enableBlocking); $("#btnUpdateBlockListsNow").prop("disabled", !enableBlocking || ($("#txtBlockListUrls").val() == "")); } function updateChart(chart, data) { chart.data = data; chart.update(); loadChartLegendSettings(chart); //Reload the chart legend } function loadChartLegendSettings(chart) { var labelFilters = localStorage.getItem("chart_" + chart.id + "_legend"); if (labelFilters != null) { labelFilters = JSON.parse(labelFilters); if (chart.config.type == "doughnut" || chart.config.type == "pie") { chart.data.labels.forEach((label, index) => { let labelFilter = labelFilters.filter(function (f) { return f.title == this.toString(); }, label); if (labelFilter.length > 0) { chart.getDatasetMeta(0).data[index].hidden = labelFilter[0].hidden; } }); } else { chart.data.datasets.forEach((data, index) => { let labelFilter = labelFilters.filter(function (f) { return f.title == this.toString(); }, data.label); if (labelFilter.length > 0) { chart.getDatasetMeta(index).hidden = labelFilter[0].hidden; } }); } chart.update(); } } function saveChartLegendSettings(chart) { var labelFilters = []; if (chart.config.type == "doughnut" || chart.config.type == "pie") { chart.data.labels.forEach((label, index) => { var hidden = chart.getDatasetMeta(0).data[index].hidden; labelFilters.push( { title: label, hidden: hidden } ); }); } else { chart.data.datasets.forEach((data, index) => { var hidden = chart.getDatasetMeta(index).hidden; labelFilters.push( { title: data.label, hidden: hidden } ); }); } localStorage.setItem("chart_" + chart.id + "_legend", JSON.stringify(labelFilters)); } var chartLegendOnClick = function (e, legendItem) { var chartType = this.chart.config.type; if (chartType == "doughnut") { Chart.defaults.doughnut.legend.onClick.call(this, e, legendItem); } else if (chartType == "pie") { Chart.defaults.pie.legend.onClick.call(this, e, legendItem); } else { Chart.defaults.global.legend.onClick.call(this, e, legendItem); } saveChartLegendSettings(this.chart); } function refreshDashboard(hideLoader) { if (!$("#mainPanelTabPaneDashboard").hasClass("active")) return; if (hideLoader == null) hideLoader = false; var divDashboardLoader = $("#divDashboardLoader"); var divDashboard = $("#divDashboard"); var type = $("input[name=rdStatType]:checked").val(); var custom = ""; if (type === "custom") { var txtStart = $("#dpCustomDayWiseStart").val(); if (txtStart === null || (txtStart === "")) { showAlert("warning", "Missing!", "Please select a start date."); $("#dpCustomDayWiseStart").trigger("focus"); return; } var txtEnd = $("#dpCustomDayWiseEnd").val(); if (txtEnd === null || (txtEnd === "")) { showAlert("warning", "Missing!", "Please select an end date."); $("#dpCustomDayWiseEnd").trigger("focus"); return; } var start = moment(txtStart); var end = moment(txtEnd); if ((end.diff(start, "days") + 1) > 7) { start = moment.utc(txtStart).toISOString(); end = moment.utc(txtEnd).toISOString(); } else { start = start.toISOString(); end = end.toISOString(); } custom = "&start=" + encodeURIComponent(start) + "&end=" + encodeURIComponent(end); } var node = $("#optDashboardClusterNode").val(); if (!hideLoader) { divDashboard.hide(); divDashboardLoader.show(); } HTTPRequest({ url: "api/dashboard/stats/get?token=" + sessionData.token + "&type=" + type + "&utc=true" + custom + "&node=" + encodeURIComponent(node), success: function (responseJSON) { //stats $("#divDashboardStatsTotalQueries").text(responseJSON.response.stats.totalQueries.toLocaleString()); $("#divDashboardStatsTotalNoError").text(responseJSON.response.stats.totalNoError.toLocaleString()); $("#divDashboardStatsTotalServerFailure").text(responseJSON.response.stats.totalServerFailure.toLocaleString()); $("#divDashboardStatsTotalNxDomain").text(responseJSON.response.stats.totalNxDomain.toLocaleString()); $("#divDashboardStatsTotalRefused").text(responseJSON.response.stats.totalRefused.toLocaleString()); $("#divDashboardStatsTotalAuthHit").text(responseJSON.response.stats.totalAuthoritative.toLocaleString()); $("#divDashboardStatsTotalRecursions").text(responseJSON.response.stats.totalRecursive.toLocaleString()); $("#divDashboardStatsTotalCacheHit").text(responseJSON.response.stats.totalCached.toLocaleString()); $("#divDashboardStatsTotalBlocked").text(responseJSON.response.stats.totalBlocked.toLocaleString()); $("#divDashboardStatsTotalDropped").text(responseJSON.response.stats.totalDropped.toLocaleString()); $("#divDashboardStatsTotalClients").text(responseJSON.response.stats.totalClients.toLocaleString()); $("#divDashboardStatsZones").text(responseJSON.response.stats.zones.toLocaleString()); $("#divDashboardStatsCachedEntries").text(responseJSON.response.stats.cachedEntries.toLocaleString()); $("#divDashboardStatsAllowedZones").text(responseJSON.response.stats.allowedZones.toLocaleString()); $("#divDashboardStatsBlockedZones").text(responseJSON.response.stats.blockedZones.toLocaleString()); $("#divDashboardStatsAllowListZones").text(responseJSON.response.stats.allowListZones.toLocaleString()); $("#divDashboardStatsBlockListZones").text(responseJSON.response.stats.blockListZones.toLocaleString()); if (responseJSON.response.stats.totalQueries > 0) { $("#divDashboardStatsTotalNoErrorPercentage").text((responseJSON.response.stats.totalNoError * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalServerFailurePercentage").text((responseJSON.response.stats.totalServerFailure * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalNxDomainPercentage").text((responseJSON.response.stats.totalNxDomain * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalRefusedPercentage").text((responseJSON.response.stats.totalRefused * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalAuthHitPercentage").text((responseJSON.response.stats.totalAuthoritative * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalRecursionsPercentage").text((responseJSON.response.stats.totalRecursive * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalCacheHitPercentage").text((responseJSON.response.stats.totalCached * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalBlockedPercentage").text((responseJSON.response.stats.totalBlocked * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); $("#divDashboardStatsTotalDroppedPercentage").text((responseJSON.response.stats.totalDropped * 100 / responseJSON.response.stats.totalQueries).toFixed(2) + "%"); } else { $("#divDashboardStatsTotalNoErrorPercentage").text("0%"); $("#divDashboardStatsTotalServerFailurePercentage").text("0%"); $("#divDashboardStatsTotalNxDomainPercentage").text("0%"); $("#divDashboardStatsTotalRefusedPercentage").text("0%"); $("#divDashboardStatsTotalAuthHitPercentage").text("0%"); $("#divDashboardStatsTotalRecursionsPercentage").text("0%"); $("#divDashboardStatsTotalCacheHitPercentage").text("0%"); $("#divDashboardStatsTotalBlockedPercentage").text("0%"); $("#divDashboardStatsTotalDroppedPercentage").text("0%"); } //main chart //fix labels switch (responseJSON.response.mainChartData.labelFormat) { case "MM/DD": case "DD/MM": case "MM/YYYY": for (var i = 0; i < responseJSON.response.mainChartData.labels.length; i++) { responseJSON.response.mainChartData.labels[i] = moment(responseJSON.response.mainChartData.labels[i]).utc().format(responseJSON.response.mainChartData.labelFormat); } break; default: for (var i = 0; i < responseJSON.response.mainChartData.labels.length; i++) { responseJSON.response.mainChartData.labels[i] = moment(responseJSON.response.mainChartData.labels[i]).local().format(responseJSON.response.mainChartData.labelFormat); } break; } if (window.chartDashboardMain == null) { var contextDashboardMain = document.getElementById("canvasDashboardMain").getContext('2d'); window.chartDashboardMain = new Chart(contextDashboardMain, { type: 'line', data: responseJSON.response.mainChartData, options: { elements: { line: { tension: 0.2, } }, scales: { yAxes: [{ ticks: { beginAtZero: true } }] }, legend: { onClick: chartLegendOnClick } } }); loadChartLegendSettings(window.chartDashboardMain); } else { updateChart(window.chartDashboardMain, responseJSON.response.mainChartData); } //query response chart if (window.chartDashboardPie == null) { var contextDashboardPie = document.getElementById("canvasDashboardPie").getContext('2d'); window.chartDashboardPie = new Chart(contextDashboardPie, { type: 'doughnut', data: responseJSON.response.queryResponseChartData, options: { legend: { onClick: chartLegendOnClick } } }); loadChartLegendSettings(window.chartDashboardPie); } else { updateChart(window.chartDashboardPie, responseJSON.response.queryResponseChartData); } //query type chart if (window.chartDashboardPie2 == null) { var contextDashboardPie2 = document.getElementById("canvasDashboardPie2").getContext('2d'); window.chartDashboardPie2 = new Chart(contextDashboardPie2, { type: 'doughnut', data: responseJSON.response.queryTypeChartData, options: { legend: { onClick: chartLegendOnClick } } }); loadChartLegendSettings(window.chartDashboardPie2); } else { updateChart(window.chartDashboardPie2, responseJSON.response.queryTypeChartData); } //protocol type chart if (window.chartDashboardPie3 == null) { var contextDashboardPie3 = document.getElementById("canvasDashboardPie3").getContext('2d'); window.chartDashboardPie3 = new Chart(contextDashboardPie3, { type: 'doughnut', data: responseJSON.response.protocolTypeChartData, options: { legend: { onClick: chartLegendOnClick } } }); loadChartLegendSettings(window.chartDashboardPie3); } else { updateChart(window.chartDashboardPie3, responseJSON.response.protocolTypeChartData); } //top clients { var tableHtmlRows; var topClients = responseJSON.response.topClients; if (topClients.length < 1) { tableHtmlRows = "No Data"; } else { tableHtmlRows = ""; for (var i = 0; i < topClients.length; i++) { tableHtmlRows += "" + htmlEncode(topClients[i].name) + (topClients[i].rateLimited ? " (rate limited)" : "") + "
    " + htmlEncode(topClients[i].domain == "" ? "." : topClients[i].domain) + "" + topClients[i].hits.toLocaleString(); tableHtmlRows += "
    "; } } $("#tableTopClients").html(tableHtmlRows); } //top domains { var tableHtmlRows; var topDomains = responseJSON.response.topDomains; if (topDomains.length < 1) { tableHtmlRows = "No Data"; } else { tableHtmlRows = ""; for (var i = 0; i < topDomains.length; i++) { if (topDomains[i].nameIdn == null) tableHtmlRows += "" + htmlEncode(topDomains[i].name == "" ? "." : topDomains[i].name) + "" + topDomains[i].hits.toLocaleString(); else tableHtmlRows += "" + htmlEncode(topDomains[i].nameIdn) + "" + topDomains[i].hits.toLocaleString(); tableHtmlRows += "
    "; } } $("#tableTopDomains").html(tableHtmlRows); } //top blocked domains { var tableHtmlRows; var topBlockedDomains = responseJSON.response.topBlockedDomains; if (topBlockedDomains.length < 1) { tableHtmlRows = "No Data"; } else { tableHtmlRows = ""; for (var i = 0; i < topBlockedDomains.length; i++) { if (topBlockedDomains[i].nameIdn == null) tableHtmlRows += "" + htmlEncode(topBlockedDomains[i].name == "" ? "." : topBlockedDomains[i].name) + "" + topBlockedDomains[i].hits.toLocaleString(); else tableHtmlRows += "" + htmlEncode(topBlockedDomains[i].nameIdn) + "" + topBlockedDomains[i].hits.toLocaleString(); tableHtmlRows += "
    "; } } $("#tableTopBlockedDomains").html(tableHtmlRows); } if (!hideLoader) { divDashboardLoader.hide(); divDashboard.show(); } }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divDashboardLoader, dontHideAlert: hideLoader }); } function showTopStats(statsType, limit) { var divTopStatsAlert = $("#divTopStatsAlert"); var divTopStatsLoader = $("#divTopStatsLoader"); $("#tableTopStatsClients").hide(); $("#tableTopStatsDomains").hide(); $("#tableTopStatsBlockedDomains").hide(); divTopStatsLoader.show(); switch (statsType) { case "TopClients": $("#lblTopStatsTitle").text("Top " + limit + " Clients"); break; case "TopDomains": $("#lblTopStatsTitle").text("Top " + limit + " Domains"); break; case "TopBlockedDomains": $("#lblTopStatsTitle").text("Top " + limit + " Blocked Domains"); break; } $("#modalTopStats").modal("show"); var type = $("input[name=rdStatType]:checked").val(); var custom = ""; if (type === "custom") { var txtStart = $("#dpCustomDayWiseStart").val(); if (txtStart === null || (txtStart === "")) { showAlert("warning", "Missing!", "Please select a start date."); $("#dpCustomDayWiseStart").trigger("focus"); return; } var txtEnd = $("#dpCustomDayWiseEnd").val(); if (txtEnd === null || (txtEnd === "")) { showAlert("warning", "Missing!", "Please select an end date."); $("#dpCustomDayWiseEnd").trigger("focus"); return; } var start = moment(txtStart); var end = moment(txtEnd); if ((end.diff(start, "days") + 1) > 7) { start = moment.utc(txtStart).toISOString(); end = moment.utc(txtEnd).toISOString(); } else { start = start.toISOString(); end = end.toISOString(); } custom = "&start=" + encodeURIComponent(start) + "&end=" + encodeURIComponent(end); } var node = $("#optDashboardClusterNode").val(); HTTPRequest({ url: "api/dashboard/stats/getTop?token=" + sessionData.token + "&type=" + type + custom + "&statsType=" + statsType + "&limit=" + limit + "&node=" + encodeURIComponent(node), success: function (responseJSON) { divTopStatsLoader.hide(); if (responseJSON.response.topClients != null) { var tableHtmlRows; var topClients = responseJSON.response.topClients; if (topClients.length < 1) { tableHtmlRows = "No Data"; } else { tableHtmlRows = ""; for (var i = 0; i < topClients.length; i++) { tableHtmlRows += "" + htmlEncode(topClients[i].name) + (topClients[i].rateLimited ? " (rate limited)" : "") + "
    " + htmlEncode(topClients[i].domain == "" ? "." : topClients[i].domain) + "" + topClients[i].hits.toLocaleString(); tableHtmlRows += "
    "; } } $("#tbodyTopStatsClients").html(tableHtmlRows); if (topClients.length > 0) $("#tfootTopStatsClients").html("Total Clients: " + topClients.length); else $("#tfootTopStatsClients").html(""); $("#tableTopStatsClients").show(); } else if (responseJSON.response.topDomains != null) { var tableHtmlRows; var topDomains = responseJSON.response.topDomains; if (topDomains.length < 1) { tableHtmlRows = "No Data"; } else { tableHtmlRows = ""; for (var i = 0; i < topDomains.length; i++) { if (topDomains[i].nameIdn == null) tableHtmlRows += "" + htmlEncode(topDomains[i].name == "" ? "." : topDomains[i].name) + "" + topDomains[i].hits.toLocaleString(); else tableHtmlRows += "" + htmlEncode(topDomains[i].nameIdn) + "" + topDomains[i].hits.toLocaleString(); tableHtmlRows += "
    "; } } $("#tbodyTopStatsDomains").html(tableHtmlRows); if (topDomains.length > 0) $("#tfootTopStatsDomains").html("Total Domains: " + topDomains.length); else $("#tfootTopStatsDomains").html(""); $("#tableTopStatsDomains").show(); } else if (responseJSON.response.topBlockedDomains != null) { var tableHtmlRows; var topBlockedDomains = responseJSON.response.topBlockedDomains; if (topBlockedDomains.length < 1) { tableHtmlRows = "No Data"; } else { tableHtmlRows = ""; for (var i = 0; i < topBlockedDomains.length; i++) { if (topBlockedDomains[i].nameIdn == null) tableHtmlRows += "" + htmlEncode(topBlockedDomains[i].name == "" ? "." : topBlockedDomains[i].name) + "" + topBlockedDomains[i].hits.toLocaleString(); else tableHtmlRows += "" + htmlEncode(topBlockedDomains[i].nameIdn) + "" + topBlockedDomains[i].hits.toLocaleString(); tableHtmlRows += "
    "; } } $("#tbodyTopStatsBlockedDomains").html(tableHtmlRows); if (topBlockedDomains.length > 0) $("#tfootTopStatsBlockedDomains").html("Total Domains: " + topBlockedDomains.length); else $("#tfootTopStatsBlockedDomains").html(""); $("#tableTopStatsBlockedDomains").show(); } $("#divTopStatsData").animate({ scrollTop: 0 }, "fast"); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divTopStatsLoader, objAlertPlaceholder: divTopStatsAlert }); } function resetBackupSettingsModal() { $("#divBackupSettingsAlert").html(""); $("#chkBackupAuthConfig").prop("checked", true); $("#chkBackupClusterConfig").prop("checked", true); $("#chkBackupWebServiceConfig").prop("checked", true); $("#chkBackupDnsConfig").prop("checked", true); $("#chkBackupLogConfig").prop("checked", true); $("#chkBackupZones").prop("checked", true); $("#chkBackupAllowedZones").prop("checked", true); $("#chkBackupBlockedZones").prop("checked", true); $("#chkBackupBlockLists").prop("checked", true); $("#chkBackupApps").prop("checked", true); $("#chkBackupScopes").prop("checked", true); $("#chkBackupStats").prop("checked", true); $("#chkBackupLogs").prop("checked", false); } function backupSettings() { var divBackupSettingsAlert = $("#divBackupSettingsAlert"); var authConfig = $("#chkBackupAuthConfig").prop("checked"); var clusterConfig = $("#chkBackupClusterConfig").prop("checked"); var webServiceSettings = $("#chkBackupWebServiceConfig").prop("checked"); var dnsSettings = $("#chkBackupDnsConfig").prop("checked"); var logSettings = $("#chkBackupLogConfig").prop("checked"); var zones = $("#chkBackupZones").prop("checked"); var allowedZones = $("#chkBackupAllowedZones").prop("checked"); var blockedZones = $("#chkBackupBlockedZones").prop("checked"); var blockLists = $("#chkBackupBlockLists").prop("checked"); var apps = $("#chkBackupApps").prop("checked"); var scopes = $("#chkBackupScopes").prop("checked"); var stats = $("#chkBackupStats").prop("checked"); var logs = $("#chkBackupLogs").prop("checked"); if (!authConfig && !clusterConfig && !webServiceSettings && !dnsSettings && !logSettings && !zones && !allowedZones && !blockedZones && !blockLists && !apps && !scopes && !stats && !logs) { showAlert("warning", "Missing!", "Please select at least one item to backup.", divBackupSettingsAlert); return; } var node = $("#optSettingsClusterNode").val(); window.open("api/settings/backup?token=" + sessionData.token + "&authConfig=" + authConfig + "&clusterConfig=" + clusterConfig + "&webServiceSettings=" + webServiceSettings + "&dnsSettings=" + dnsSettings + "&logSettings=" + logSettings + "&zones=" + zones + "&allowedZones=" + allowedZones + "&blockedZones=" + blockedZones + "&blockLists=" + blockLists + "&apps=" + apps + "&scopes=" + scopes + "&stats=" + stats + "&logs=" + logs + "&node=" + encodeURIComponent(node) + "&ts=" + (new Date().getTime()), "_blank"); $("#modalBackupSettings").modal("hide"); showAlert("success", "Backed Up!", "Settings were backed up successfully."); } function resetRestoreSettingsModal() { $("#divRestoreSettingsAlert").html(""); $("#fileBackupZip").val(""); $("#chkRestoreAuthConfig").prop("checked", true); $("#chkRestoreClusterConfig").prop("checked", true); $("#chkRestoreWebServiceConfig").prop("checked", true); $("#chkRestoreDnsConfig").prop("checked", true); $("#chkRestoreLogConfig").prop("checked", true); $("#chkRestoreZones").prop("checked", true); $("#chkRestoreAllowedZones").prop("checked", true); $("#chkRestoreBlockedZones").prop("checked", true); $("#chkRestoreBlockLists").prop("checked", true); $("#chkRestoreApps").prop("checked", true); $("#chkRestoreScopes").prop("checked", true); $("#chkRestoreStats").prop("checked", true); $("#chkRestoreLogs").prop("checked", false); $("#chkDeleteExistingFiles").prop("checked", true); } function restoreSettings() { var divRestoreSettingsAlert = $("#divRestoreSettingsAlert"); var fileBackupZip = $("#fileBackupZip"); if (fileBackupZip[0].files.length === 0) { showAlert("warning", "Missing!", "Please select a backup zip file to restore.", divRestoreSettingsAlert); fileBackupZip.trigger("focus"); return; } var authConfig = $("#chkRestoreAuthConfig").prop("checked"); var clusterConfig = $("#chkRestoreClusterConfig").prop("checked"); var webServiceSettings = $("#chkRestoreWebServiceConfig").prop("checked"); var dnsSettings = $("#chkRestoreDnsConfig").prop("checked"); var logSettings = $("#chkRestoreLogConfig").prop("checked"); var zones = $("#chkRestoreZones").prop("checked"); var allowedZones = $("#chkRestoreAllowedZones").prop("checked"); var blockedZones = $("#chkRestoreBlockedZones").prop("checked"); var blockLists = $("#chkRestoreBlockLists").prop("checked"); var apps = $("#chkRestoreApps").prop("checked"); var scopes = $("#chkRestoreScopes").prop("checked"); var stats = $("#chkRestoreStats").prop("checked"); var logs = $("#chkRestoreLogs").prop("checked"); var deleteExistingFiles = $("#chkDeleteExistingFiles").prop("checked"); if (!authConfig && !clusterConfig && !webServiceSettings && !dnsSettings && !logSettings && !zones && !allowedZones && !blockedZones && !blockLists && !apps && !scopes && !stats && !logs) { showAlert("warning", "Missing!", "Please select at least one item to restore.", divRestoreSettingsAlert); return; } var formData = new FormData(); formData.append("fileBackupZip", $("#fileBackupZip")[0].files[0]); var node = $("#optSettingsClusterNode").val(); var btn = $("#btnRestoreSettings"); btn.button("loading"); HTTPRequest({ url: "api/settings/restore?token=" + sessionData.token + "&authConfig=" + authConfig + "&clusterConfig=" + clusterConfig + "&webServiceSettings=" + webServiceSettings + "&dnsSettings=" + dnsSettings + "&logSettings=" + logSettings + "&zones=" + zones + "&allowedZones=" + allowedZones + "&blockedZones=" + blockedZones + "&blockLists=" + blockLists + "&apps=" + apps + "&scopes=" + scopes + "&stats=" + stats + "&logs=" + logs + "&deleteExistingFiles=" + deleteExistingFiles + "&node=" + encodeURIComponent(node), method: "POST", data: formData, contentType: false, processData: false, success: function (responseJSON) { if ((node == "") || (node == sessionData.info.dnsServerDomain)) updateDnsSettingsDataAndGui(responseJSON); loadDnsSettings(responseJSON); $("#modalRestoreSettings").modal("hide"); btn.button("reset"); showAlert("success", "Restored!", "Settings were restored successfully."); if (sessionData.info.dnsServerDomain == responseJSON.server) checkForWebConsoleRedirection(responseJSON); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divRestoreSettingsAlert }); } function applyTheme() { const currentTheme = localStorage.getItem("theme"); if (currentTheme === "dark") document.body.classList.add("dark-mode"); else document.body.classList.remove("dark-mode"); } function toggleTheme() { document.body.classList.toggle("dark-mode"); let theme = "light"; if (document.body.classList.contains("dark-mode")) theme = "dark"; localStorage.setItem("theme", theme); if (window.chartDashboardMain) { window.chartDashboardMain.update(); window.chartDashboardPie.update(); window.chartDashboardPie2.update(); window.chartDashboardPie3.update(); } } ================================================ FILE: DnsServerCore/www/js/other-zones.js ================================================ /* 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 . */ function flushDnsCache(objBtn, node) { if (!confirm("Are you sure to flush the DNS Server cache?")) return; var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/cache/flush?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#lstCachedZones").html(""); $("#txtCachedZoneViewerTitle").text(""); $("#btnDeleteCachedZone").hide(); $("#preCachedZoneViewerBody").hide(); btn.button("reset"); showAlert("success", "Flushed!", "DNS Server cache was flushed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function deleteCachedZone() { var domain = $("#txtCachedZoneViewerTitle").text(); if (!confirm("Are you sure you want to delete the cached zone '" + domain + "' and all its records?")) return; var node = $("#optCachedZonesClusterNode").val(); var btn = $("#btnDeleteCachedZone"); btn.button("loading"); HTTPRequest({ url: "api/cache/delete?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshCachedZonesList(getParentDomain(domain), "up"); btn.button("reset"); showAlert("success", "Deleted!", "Cached zone '" + domain + "' was deleted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function getParentDomain(domain) { if ((domain != null) && (domain != "")) { var parentDomain; var i = domain.indexOf("."); if (i == -1) parentDomain = ""; else parentDomain = domain.substr(i + 1); return parentDomain; } return null; } function refreshCachedZonesList(domain, direction) { if (domain == null) { domain = $("#txtCachedZoneViewerTitle").text(); if ((domain == null) || (domain == "")) domain = ""; } domain.toLowerCase(); var node = $("#optCachedZonesClusterNode").val(); var lstCachedZones = $("#lstCachedZones"); var divCachedZoneViewer = $("#divCachedZoneViewer"); var preCachedZoneViewerBody = $("#preCachedZoneViewerBody"); divCachedZoneViewer.hide(); preCachedZoneViewerBody.hide(); HTTPRequest({ url: "api/cache/list?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain) + ((direction == null) ? "" : "&direction=" + direction) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var newDomain = responseJSON.response.domain; var zones = responseJSON.response.zones; var list = ""; var parentDomain = getParentDomain(newDomain); if (parentDomain != null) list += ""; for (var i = 0; i < zones.length; i++) { var zoneName = htmlEncode(zones[i]); list += ""; } lstCachedZones.html(list); if (newDomain == "") { $("#txtCachedZoneViewerTitle").text(""); $("#btnDeleteCachedZone").hide(); } else { if (responseJSON.response.domainIdn == null) $("#txtCachedZoneViewerTitle").text(newDomain); else $("#txtCachedZoneViewerTitle").text(responseJSON.response.domainIdn); $("#btnDeleteCachedZone").show(); } if (responseJSON.response.records.length > 0) { preCachedZoneViewerBody.text(JSON.stringify(responseJSON.response.records, null, 2)); preCachedZoneViewerBody.show(); } divCachedZoneViewer.show(); }, invalidToken: function () { showPageLogin(); }, error: function () { lstCachedZones.html(""); divCachedZoneViewer.show(); }, objLoaderPlaceholder: lstCachedZones }); } function allowZone() { var domain = $("#txtAllowZone").val(); if ((domain === null) || (domain === "")) { showAlert("warning", "Missing!", "Please enter a domain name to allow."); $("#txtAllowZone").trigger("focus"); return; } var btn = $("#btnAllowZone"); btn.button("loading"); HTTPRequest({ url: "api/allowed/add?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { refreshAllowedZonesList(domain, null, true); $("#txtAllowZone").val(""); btn.button("reset"); showAlert("success", "Allowed!", "Domain '" + domain + "' was added to Allowed Zone successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function deleteAllowedZone() { var domain = $("#txtAllowedZoneViewerTitle").text(); if (!confirm("Are you sure you want to delete the allowed zone '" + domain + "'?")) return; var btn = $("#btnDeleteAllowedZone"); btn.button("loading"); HTTPRequest({ url: "api/allowed/delete?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { refreshAllowedZonesList(getParentDomain(domain), "up", true); btn.button("reset"); showAlert("success", "Deleted!", "Domain '" + domain + "' was deleted from Allowed Zone successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function flushAllowedZone() { if (!confirm("Are you sure you want to flush the entire Allowed zone?")) return; var btn = $("#btnFlushAllowedZone"); btn.button("loading"); HTTPRequest({ url: "api/allowed/flush?token=" + sessionData.token, success: function (responseJSON) { $("#lstAllowedZones").html(""); $("#txtAllowedZoneViewerTitle").text(""); $("#btnDeleteAllowedZone").hide(); $("#preAllowedZoneViewerBody").hide(); btn.button("reset"); showAlert("success", "Flushed!", "Allowed zone was flushed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function refreshAllowedZonesList(domain, direction, fromPrimary) { if (domain == null) { domain = $("#txtAllowedZoneViewerTitle").text(); if ((domain == null) || (domain == "")) domain = ""; } domain.toLowerCase(); var node = fromPrimary ? getPrimaryClusterNodeName() : ""; var lstAllowedZones = $("#lstAllowedZones"); var divAllowedZoneViewer = $("#divAllowedZoneViewer"); var preAllowedZoneViewerBody = $("#preAllowedZoneViewerBody"); divAllowedZoneViewer.hide(); preAllowedZoneViewerBody.hide(); HTTPRequest({ url: "api/allowed/list?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain) + ((direction == null) ? "" : "&direction=" + direction) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var newDomain = responseJSON.response.domain; var zones = responseJSON.response.zones; var list = ""; var parentDomain = getParentDomain(newDomain); if (parentDomain != null) list += ""; for (var i = 0; i < zones.length; i++) { var zoneName = htmlEncode(zones[i]); list += ""; } lstAllowedZones.html(list); if (newDomain == "") { $("#txtAllowedZoneViewerTitle").text(""); } else { if (responseJSON.response.domainIdn == null) $("#txtAllowedZoneViewerTitle").text(newDomain); else $("#txtAllowedZoneViewerTitle").text(responseJSON.response.domainIdn); } if (responseJSON.response.records.length > 0) { preAllowedZoneViewerBody.text(JSON.stringify(responseJSON.response.records, null, 2)); preAllowedZoneViewerBody.show(); $("#btnDeleteAllowedZone").show(); } else { $("#btnDeleteAllowedZone").hide(); } divAllowedZoneViewer.show(); }, invalidToken: function () { showPageLogin(); }, error: function () { lstAllowedZones.html(""); divAllowedZoneViewer.show(); }, objLoaderPlaceholder: lstAllowedZones }); } function blockZone() { var domain = $("#txtBlockZone").val(); if ((domain === null) || (domain === "")) { showAlert("warning", "Missing!", "Please enter a domain name to block."); $("#txtBlockZone").trigger("focus"); return; } var btn = $("#btnBlockZone"); btn.button("loading"); HTTPRequest({ url: "api/blocked/add?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { refreshBlockedZonesList(domain, null, true); $("#txtBlockZone").val(""); btn.button("reset"); showAlert("success", "Blocked!", "Domain '" + domain + "' was added to Blocked Zone successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function deleteBlockedZone() { var domain = $("#txtBlockedZoneViewerTitle").text(); if (!confirm("Are you sure you want to delete the blocked zone '" + domain + "'?")) return; var btn = $("#btnDeleteBlockedZone"); btn.button("loading"); HTTPRequest({ url: "api/blocked/delete?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { refreshBlockedZonesList(getParentDomain(domain), "up", true); btn.button("reset"); showAlert("success", "Deleted!", "Blocked zone '" + domain + "' was deleted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function flushBlockedZone() { if (!confirm("Are you sure you want to flush the entire Blocked zone?")) return; var btn = $("#btnFlushBlockedZone"); btn.button("loading"); HTTPRequest({ url: "api/blocked/flush?token=" + sessionData.token, success: function (responseJSON) { $("#lstBlockedZones").html(""); $("#txtBlockedZoneViewerTitle").text(""); $("#btnDeleteBlockedZone").hide(); $("#preBlockedZoneViewerBody").hide(); btn.button("reset"); showAlert("success", "Flushed!", "Blocked zone was flushed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function refreshBlockedZonesList(domain, direction, fromPrimary) { if (domain == null) { domain = $("#txtBlockedZoneViewerTitle").text(); if ((domain == null) || (domain == "")) domain = ""; } domain.toLowerCase(); var node = fromPrimary ? getPrimaryClusterNodeName() : ""; var lstBlockedZones = $("#lstBlockedZones"); var divBlockedZoneViewer = $("#divBlockedZoneViewer"); var preBlockedZoneViewerBody = $("#preBlockedZoneViewerBody"); divBlockedZoneViewer.hide(); preBlockedZoneViewerBody.hide(); HTTPRequest({ url: "api/blocked/list?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain) + ((direction == null) ? "" : "&direction=" + direction) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var newDomain = responseJSON.response.domain; var zones = responseJSON.response.zones; var list = ""; var parentDomain = getParentDomain(newDomain); if (parentDomain != null) list += ""; for (var i = 0; i < zones.length; i++) { var zoneName = htmlEncode(zones[i]); list += ""; } lstBlockedZones.html(list); if (newDomain == "") { $("#txtBlockedZoneViewerTitle").text(""); } else { if (responseJSON.response.domainIdn == null) $("#txtBlockedZoneViewerTitle").text(newDomain); else $("#txtBlockedZoneViewerTitle").text(responseJSON.response.domainIdn); } if (responseJSON.response.records.length > 0) { preBlockedZoneViewerBody.text(JSON.stringify(responseJSON.response.records, null, 2)); preBlockedZoneViewerBody.show(); $("#btnDeleteBlockedZone").show(); } else { $("#btnDeleteBlockedZone").hide(); } divBlockedZoneViewer.show(); }, invalidToken: function () { showPageLogin(); }, error: function () { lstBlockedZones.html(""); divBlockedZoneViewer.show(); }, objLoaderPlaceholder: lstBlockedZones }); } function resetImportAllowedZonesModal() { $("#divImportAllowedZonesAlert").html(""); $("#txtImportAllowedZones").val(""); setTimeout(function () { $("#txtImportAllowedZones").trigger("focus"); }, 1000); } function importAllowedZones() { var divImportAllowedZonesAlert = $("#divImportAllowedZonesAlert"); var allowedZones = cleanTextList($("#txtImportAllowedZones").val()); if ((allowedZones.length === 0) || (allowedZones === ",")) { showAlert("warning", "Missing!", "Please enter allowed zones to import.", divImportAllowedZonesAlert); $("#txtImportAllowedZones").trigger("focus"); return; } var btn = $("#btnImportAllowedZones"); btn.button("loading"); HTTPRequest({ url: "api/allowed/import?token=" + sessionData.token, method: "POST", data: "allowedZones=" + encodeURIComponent(allowedZones), processData: false, success: function (responseJSON) { $("#modalImportAllowedZones").modal("hide"); btn.button("reset"); showAlert("success", "Imported!", "Domain names were imported into allowed zone successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divImportAllowedZonesAlert }); } function exportAllowedZones() { window.open("api/allowed/export?token=" + sessionData.token, "_blank"); showAlert("success", "Exported!", "Allowed zones were exported successfully."); } function resetImportBlockedZonesModal() { $("#divImportBlockedZonesAlert").html(""); $("#txtImportBlockedZones").val(""); setTimeout(function () { $("#txtImportBlockedZones").trigger("focus"); }, 1000); } function importBlockedZones() { var divImportBlockedZonesAlert = $("#divImportBlockedZonesAlert"); var blockedZones = cleanTextList($("#txtImportBlockedZones").val()); if ((blockedZones.length === 0) || (blockedZones === ",")) { showAlert("warning", "Missing!", "Please enter blocked zones to import.", divImportBlockedZonesAlert); $("#txtImportBlockedZones").trigger("focus"); return; } var btn = $("#btnImportBlockedZones"); btn.button("loading"); HTTPRequest({ url: "api/blocked/import?token=" + sessionData.token, method: "POST", data: "blockedZones=" + encodeURIComponent(blockedZones), processData: false, success: function (responseJSON) { $("#modalImportBlockedZones").modal("hide"); btn.button("reset"); showAlert("success", "Imported!", "Domain names were imported into blocked zone successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); }, objAlertPlaceholder: divImportBlockedZonesAlert }); } function exportBlockedZones() { window.open("api/blocked/export?token=" + sessionData.token, "_blank"); showAlert("success", "Exported!", "Blocked zones were exported successfully."); } function allowDomain(objMenuItem, btnName, alertPlaceholderName) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var domain = mnuItem.attr("data-domain"); var btn = $("#" + btnName + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); var alertPlaceholder; if (alertPlaceholderName != null) alertPlaceholder = $("#" + alertPlaceholderName); HTTPRequest({ url: "api/blocked/delete?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { HTTPRequest({ url: "api/allowed/add?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); showAlert("success", "Allowed!", "Domain '" + domain + "' was added to Allowed Zone successfully.", alertPlaceholder); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); }, objAlertPlaceholder: alertPlaceholder }); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); }, objAlertPlaceholder: alertPlaceholder }); } function blockDomain(objMenuItem, btnName, alertPlaceholderName) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var domain = mnuItem.attr("data-domain"); var btn = $("#" + btnName + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); var alertPlaceholder; if (alertPlaceholderName != null) alertPlaceholder = $("#" + alertPlaceholderName); HTTPRequest({ url: "api/allowed/delete?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { HTTPRequest({ url: "api/blocked/add?token=" + sessionData.token + "&domain=" + encodeURIComponent(domain), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); showAlert("success", "Blocked!", "Domain '" + domain + "' was added to Blocked Zone successfully.", alertPlaceholder); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); }, objAlertPlaceholder: alertPlaceholder }); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); }, objAlertPlaceholder: alertPlaceholder }); } ================================================ FILE: DnsServerCore/www/js/zone.js ================================================ /* 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 . */ var zoneOptionsAvailableTsigKeyNames; var editZoneInfo; var editZoneRecords; var editZoneFilteredRecords; $(function () { $("input[type=radio][name=rdAddZoneType]").on("change", function () { $("#txtAddZone").prop("disabled", false); $("#divAddZoneCatalogZone").hide(); $("#divAddZoneInitializeForwarder").hide(); $("#divAddZoneImportZoneFile").hide(); $("#divAddZoneUseSoaSerialDateScheme").hide(); $("#divAddZonePrimaryNameServerAddresses").hide(); $("#lblAddZonePrimaryNameServerAddresses").text("Primary Name Server Addresses (Optional)"); $("#divAddZonePrimaryNameServerAddressesInfo").text("Enter the primary name server addresses to sync the zone from. When unspecified, the SOA Primary Name Server will be resolved and used."); $("#divAddZoneZoneTransferProtocol").hide(); $("#divAddZoneTsigKeyName").hide(); $("#divAddZoneValidateZone").hide(); $("#divAddZoneForwarderProtocol").hide(); $("#divAddZoneForwarder").hide(); $("#divAddZoneForwarderDnssecValidation").hide(); $("#divAddZoneForwarderProxy").hide(); var zoneType = $('input[name=rdAddZoneType]:checked').val(); switch (zoneType) { case "Primary": if ($("#optAddZoneCatalogZoneName").attr("hasItems") == "true") $("#divAddZoneCatalogZone").show(); $("#divAddZoneImportZoneFile").show(); $("#divAddZoneUseSoaSerialDateScheme").show(); break; case "Secondary": if ($("#optAddZoneCatalogZoneName").attr("hasItems") == "true") $("#divAddZoneCatalogZone").show(); $("#divAddZonePrimaryNameServerAddresses").show(); $("#divAddZoneZoneTransferProtocol").show(); $("#divAddZoneTsigKeyName").show(); $("#divAddZoneValidateZone").show(); loadTsigKeyNames($("#optAddZoneTsigKeyName"), null, $("#divAddZoneAlert")); break; case "Stub": if ($("#optAddZoneCatalogZoneName").attr("hasItems") == "true") $("#divAddZoneCatalogZone").show(); $("#divAddZonePrimaryNameServerAddresses").show(); break; case "Forwarder": if ($("#optAddZoneCatalogZoneName").attr("hasItems") == "true") $("#divAddZoneCatalogZone").show(); $("#divAddZoneInitializeForwarder").show(); var initializeForwarder = $("#chkAddZoneInitializeForwarder").prop("checked"); if (initializeForwarder) { $("#divAddZoneImportZoneFile").hide(); $("#divAddZoneForwarderProtocol").show(); $("#divAddZoneForwarder").show(); $("#divAddZoneForwarderDnssecValidation").show(); $("#divAddZoneForwarderProxy").show(); } else { $("#divAddZoneImportZoneFile").show(); $("#divAddZoneForwarderProtocol").hide(); $("#divAddZoneForwarder").hide(); $("#divAddZoneForwarderDnssecValidation").hide(); $("#divAddZoneForwarderProxy").hide(); } break; case "SecondaryForwarder": case "SecondaryCatalog": $("#lblAddZonePrimaryNameServerAddresses").text("Primary Name Server Addresses"); $("#divAddZonePrimaryNameServerAddressesInfo").text("Enter the primary name server addresses to sync the zone from."); $("#divAddZonePrimaryNameServerAddresses").show(); $("#divAddZoneZoneTransferProtocol").show(); $("#divAddZoneTsigKeyName").show(); loadTsigKeyNames($("#optAddZoneTsigKeyName"), null, $("#divAddZoneAlert")); break; case "SecondaryRoot": if ($("#optAddZoneCatalogZoneName").attr("hasItems") == "true") $("#divAddZoneCatalogZone").show(); $("#txtAddZone").prop("disabled", true); $("#txtAddZone").val("."); break; } }); $("#chkAddZoneInitializeForwarder").on("click", function () { var initializeForwarder = $("#chkAddZoneInitializeForwarder").prop("checked"); if (initializeForwarder) { $("#divAddZoneImportZoneFile").hide(); $("#divAddZoneForwarderProtocol").show(); $("#divAddZoneForwarder").show(); $("#divAddZoneForwarderDnssecValidation").show(); $("#divAddZoneForwarderProxy").show(); } else { $("#divAddZoneImportZoneFile").show(); $("#divAddZoneForwarderProtocol").hide(); $("#divAddZoneForwarder").hide(); $("#divAddZoneForwarderDnssecValidation").hide(); $("#divAddZoneForwarderProxy").hide(); } }); $("input[type=radio][name=rdAddZoneForwarderProtocol]").on("change", function () { var protocol = $('input[name=rdAddZoneForwarderProtocol]:checked').val(); switch (protocol) { case "Udp": case "Tcp": $("#txtAddZoneForwarder").attr("placeholder", "8.8.8.8 or [2620:fe::10]") break; case "Tls": case "Quic": $("#txtAddZoneForwarder").attr("placeholder", "dns.quad9.net (9.9.9.9:853)") break; case "Https": $("#txtAddZoneForwarder").attr("placeholder", "https://cloudflare-dns.com/dns-query (1.1.1.1)") break; } }); $("input[type=radio][name=rdAddZoneForwarderProxyType]").on("change", function () { var proxyType = $('input[name=rdAddZoneForwarderProxyType]:checked').val(); var disabled = (proxyType === "NoProxy") || (proxyType === "DefaultProxy"); $("#txtAddZoneForwarderProxyAddress").prop("disabled", disabled); $("#txtAddZoneForwarderProxyPort").prop("disabled", disabled); $("#txtAddZoneForwarderProxyUsername").prop("disabled", disabled); $("#txtAddZoneForwarderProxyPassword").prop("disabled", disabled); }); $("#txtEditZoneFilterName").on("input", function () { editZoneFilteredRecords = null; //to evaluate filters again }); $("#txtEditZoneFilterType").on("input", function () { editZoneFilteredRecords = null; //to evaluate filters again }); $("input[type=radio][name=rdImportZoneType]").on("change", function () { var rdImportZoneType = $("input[name=rdImportZoneType]:checked").val(); switch (rdImportZoneType) { case "File": $("#divImportZoneFile").show(); $("#divImportZoneTextEditor").hide(); break; case "Text": $("#divImportZoneFile").hide(); $("#divImportZoneTextEditor").show(); break; } }); $("#optZoneOptionsCatalogZoneName").on("change", function () { $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", false); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("checked", false); $("#chkZoneOptionsCatalogOverrideNotify").prop("checked", false); var catalog = $("#optZoneOptionsCatalogZoneName").val(); if (catalog === "") { $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", true); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("disabled", true); $("#chkZoneOptionsCatalogOverrideNotify").prop("disabled", true); switch ($("#lblZoneOptionsZoneName").attr("data-zone-type")) { case "Primary": case "Forwarder": $("#tabListZoneOptionsQueryAccess").show(); $("#tabListZoneOptionsZoneTranfer").show(); $("#tabListZoneOptionsNotify").show(); break; case "Secondary": $("#tabListZoneOptionsQueryAccess").show(); $("#tabListZoneOptionsZoneTranfer").show(); $("#tabListZoneOptionsNotify").show(); break; case "Stub": $("#tabListZoneOptionsQueryAccess").show(); break; } } else { switch ($("#lblZoneOptionsZoneName").attr("data-zone-type")) { case "Primary": case "Forwarder": $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", false); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("disabled", false); $("#chkZoneOptionsCatalogOverrideNotify").prop("disabled", false); $("#tabListZoneOptionsQueryAccess").hide(); $("#tabListZoneOptionsZoneTranfer").hide(); $("#tabListZoneOptionsNotify").hide(); break; case "Secondary": $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", false); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("disabled", false); $("#tabListZoneOptionsQueryAccess").hide(); $("#tabListZoneOptionsZoneTranfer").hide(); break; case "Stub": $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", false); $("#tabListZoneOptionsQueryAccess").hide(); $("#tabListZoneOptionsZoneTranfer").hide(); $("#tabListZoneOptionsNotify").hide(); break; } } }); $("#chkZoneOptionsCatalogOverrideQueryAccess").on("click", function () { var checked = $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked"); if (checked) $("#tabListZoneOptionsQueryAccess").show(); else $("#tabListZoneOptionsQueryAccess").hide(); }); $("#chkZoneOptionsCatalogOverrideZoneTransfer").on("click", function () { var checked = $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("checked"); if (checked) $("#tabListZoneOptionsZoneTranfer").show(); else $("#tabListZoneOptionsZoneTranfer").hide(); }); $("#chkZoneOptionsCatalogOverrideNotify").on("click", function () { var checked = $("#chkZoneOptionsCatalogOverrideNotify").prop("checked"); if (checked) $("#tabListZoneOptionsNotify").show(); else $("#tabListZoneOptionsNotify").hide(); }); $("input[type=radio][name=rdQueryAccess]").on("change", function () { var queryAccess = $("input[name=rdQueryAccess]:checked").val(); switch (queryAccess) { case "UseSpecifiedNetworkACL": case "AllowZoneNameServersAndUseSpecifiedNetworkACL": $("#txtQueryAccessNetworkACL").prop("disabled", false); break; default: $("#txtQueryAccessNetworkACL").prop("disabled", true); break; } }); $("input[type=radio][name=rdZoneTransfer]").on("change", function () { var zoneTransfer = $('input[name=rdZoneTransfer]:checked').val(); switch (zoneTransfer) { case "UseSpecifiedNetworkACL": case "AllowZoneNameServersAndUseSpecifiedNetworkACL": $("#txtZoneTransferNetworkACL").prop("disabled", false); break; default: $("#txtZoneTransferNetworkACL").prop("disabled", true); break; } }); $("input[type=radio][name=rdZoneNotify]").on("change", function () { var zoneNotify = $('input[name=rdZoneNotify]:checked').val(); switch (zoneNotify) { case "SpecifiedNameServers": case "BothZoneAndSpecifiedNameServers": $("#txtZoneNotifyNameServers").prop("disabled", false); $("#txtZoneNotifySecondaryCatalogNameServers").prop("disabled", true); break; case "SeparateNameServersForCatalogAndMemberZones": $("#txtZoneNotifyNameServers").prop("disabled", false); $("#txtZoneNotifySecondaryCatalogNameServers").prop("disabled", false); break; default: $("#txtZoneNotifyNameServers").prop("disabled", true); $("#txtZoneNotifySecondaryCatalogNameServers").prop("disabled", true); break; } }); $("input[type=radio][name=rdDynamicUpdate]").on("change", function () { var dynamicUpdate = $('input[name=rdDynamicUpdate]:checked').val(); switch (dynamicUpdate) { case "UseSpecifiedNetworkACL": case "AllowZoneNameServersAndUseSpecifiedNetworkACL": $("#txtDynamicUpdateNetworkACL").prop("disabled", false); break; default: $("#txtDynamicUpdateNetworkACL").prop("disabled", true); break; } }); $("input[type=radio][name=rdDnssecSignZoneAlgorithm]").on("change", function () { var algorithm = $("input[name=rdDnssecSignZoneAlgorithm]:checked").val(); switch (algorithm) { case "RSA": $("#divDnssecSignZoneRsaParameters").show(); $("#divDnssecSignZoneEcdsaParameters").hide(); $("#divDnssecSignZoneEddsaParameters").hide(); if ($("input[name=rdDnssecSignZoneKskGeneration]:checked").val() === "Automatic") $("#divDnssecSignZoneRsaKskKeySize").show(); else $("#divDnssecSignZoneRsaKskKeySize").hide(); if ($("input[name=rdDnssecSignZoneZskGeneration]:checked").val() === "Automatic") $("#divDnssecSignZoneRsaZskKeySize").show(); else $("#divDnssecSignZoneRsaZskKeySize").hide(); break; case "ECDSA": $("#divDnssecSignZoneRsaParameters").hide(); $("#divDnssecSignZoneEcdsaParameters").show(); $("#divDnssecSignZoneEddsaParameters").hide(); $("#divDnssecSignZoneRsaKskKeySize").hide(); $("#divDnssecSignZoneRsaZskKeySize").hide(); break; case "EDDSA": $("#divDnssecSignZoneRsaParameters").hide(); $("#divDnssecSignZoneEcdsaParameters").hide(); $("#divDnssecSignZoneEddsaParameters").show(); $("#divDnssecSignZoneRsaKskKeySize").hide(); $("#divDnssecSignZoneRsaZskKeySize").hide(); break; } }); $("input[type=radio][name=rdDnssecSignZoneKskGeneration]").on("change", function () { var rdDnssecSignZoneKskGeneration = $("input[name=rdDnssecSignZoneKskGeneration]:checked").val(); switch (rdDnssecSignZoneKskGeneration) { case "Automatic": if ($("input[name=rdDnssecSignZoneAlgorithm]:checked").val() === "RSA") $("#divDnssecSignZoneRsaKskKeySize").show(); else $("#divDnssecSignZoneRsaKskKeySize").hide(); $("#divDnssecSignZonePemKskPrivateKey").hide(); break; case "UseSpecified": $("#divDnssecSignZoneRsaKskKeySize").hide(); $("#divDnssecSignZonePemKskPrivateKey").show(); break; } $("#txtDnssecSignZonePemKskPrivateKey").val(""); }); $("input[type=radio][name=rdDnssecSignZoneZskGeneration]").on("change", function () { var rdDnssecSignZoneZskGeneration = $("input[name=rdDnssecSignZoneZskGeneration]:checked").val(); switch (rdDnssecSignZoneZskGeneration) { case "Automatic": if ($("input[name=rdDnssecSignZoneAlgorithm]:checked").val() === "RSA") $("#divDnssecSignZoneRsaZskKeySize").show(); else $("#divDnssecSignZoneRsaZskKeySize").hide(); $("#divDnssecSignZonePemZskPrivateKey").hide(); $("#txtDnssecSignZoneZskAutoRollover").val("30"); break; case "UseSpecified": $("#divDnssecSignZoneRsaZskKeySize").hide(); $("#divDnssecSignZonePemZskPrivateKey").show(); $("#txtDnssecSignZoneZskAutoRollover").val("0"); break; } $("#txtDnssecSignZonePemZskPrivateKey").val(""); }); $("input[type=radio][name=rdDnssecSignZoneNxProof]").on("change", function () { var nxProof = $("input[name=rdDnssecSignZoneNxProof]:checked").val(); switch (nxProof) { case "NSEC": $("#divDnssecSignZoneNSEC3Parameters").hide(); break; case "NSEC3": $("#divDnssecSignZoneNSEC3Parameters").show(); break; } }); $("#optDnssecPropertiesAddKeyKeyType").on("change", function () { var keyType = $("#optDnssecPropertiesAddKeyKeyType").val(); switch (keyType) { case "ZoneSigningKey": $("#divDnssecPropertiesAddKeyAutomaticRollover").show(); if ($("input[name=rdDnssecPropertiesKeyGeneration]:checked").val() === "Automatic") $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(30); else $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(0); break; default: $("#divDnssecPropertiesAddKeyAutomaticRollover").hide(); $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(0); break; } }); $("#optDnssecPropertiesAddKeyAlgorithm").on("change", function () { var algorithm = $("#optDnssecPropertiesAddKeyAlgorithm").val(); switch (algorithm) { case "RSA": $("#divDnssecPropertiesAddKeyRsaParameters").show(); $("#divDnssecPropertiesAddKeyEcdsaParameters").hide(); $("#divDnssecPropertiesAddKeyEddsaParameters").hide(); if ($("input[name=rdDnssecPropertiesKeyGeneration]:checked").val() === "Automatic") $("#divDnssecPropertiesAddKeyRsaKeySize").show(); else $("#divDnssecPropertiesAddKeyRsaKeySize").hide(); break; case "ECDSA": $("#divDnssecPropertiesAddKeyRsaParameters").hide(); $("#divDnssecPropertiesAddKeyEcdsaParameters").show(); $("#divDnssecPropertiesAddKeyEddsaParameters").hide(); $("#divDnssecPropertiesAddKeyRsaKeySize").hide(); break; case "EDDSA": $("#divDnssecPropertiesAddKeyRsaParameters").hide(); $("#divDnssecPropertiesAddKeyEcdsaParameters").hide(); $("#divDnssecPropertiesAddKeyEddsaParameters").show(); $("#divDnssecPropertiesAddKeyRsaKeySize").hide(); break; } }); $("input[type=radio][name=rdDnssecPropertiesKeyGeneration]").on("change", function () { var rdDnssecPropertiesKeyGeneration = $("input[name=rdDnssecPropertiesKeyGeneration]:checked").val(); switch (rdDnssecPropertiesKeyGeneration) { case "Automatic": if ($("#optDnssecPropertiesAddKeyAlgorithm").val() == "RSA") $("#divDnssecPropertiesAddKeyRsaKeySize").show(); else $("#divDnssecPropertiesAddKeyRsaKeySize").hide(); $("#divDnssecPropertiesPemPrivateKey").hide(); var keyType = $("#optDnssecPropertiesAddKeyKeyType").val(); if (keyType == "ZoneSigningKey") $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(30); else $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(0); break; case "UseSpecified": $("#divDnssecPropertiesAddKeyRsaKeySize").hide(); $("#divDnssecPropertiesPemPrivateKey").show(); $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(0); break; } $("#txtDnssecPropertiesPemPrivateKey").val(""); }); $("input[type=radio][name=rdDnssecPropertiesNxProof]").on("change", function () { var nxProof = $("input[name=rdDnssecPropertiesNxProof]:checked").val(); switch (nxProof) { case "NSEC": $("#divDnssecPropertiesNSEC3Parameters").hide(); break; case "NSEC3": $("#divDnssecPropertiesNSEC3Parameters").show(); break; } }); $("#chkAddEditRecordDataPtr").on("click", function () { var addPtrRecord = $("#chkAddEditRecordDataPtr").prop('checked'); $("#chkAddEditRecordDataCreatePtrZone").prop('disabled', !addPtrRecord); }); $("#chkAddEditRecordDataTxtSplitText").on("click", function () { var splitText = $("#chkAddEditRecordDataTxtSplitText").prop("checked"); if (!splitText) { var text = $("#txtAddEditRecordDataTxt").val(); text = text.replace(/\n/g, ""); $("#txtAddEditRecordDataTxt").val(text); } }); $("input[type=radio][name=rdAddEditRecordDataForwarderProtocol]").on("change", updateAddEditFormForwarderPlaceholder); $("input[type=radio][name=rdAddEditRecordDataForwarderProxyType]").on("change", updateAddEditFormForwarderProxyType); $("#optAddEditRecordDataAppName").on("change", function () { if (appsList == null) return; var appName = $("#optAddEditRecordDataAppName").val(); var optClassPaths = ""; for (var i = 0; i < appsList.length; i++) { if (appsList[i].name == appName) { for (var j = 0; j < appsList[i].dnsApps.length; j++) { if (appsList[i].dnsApps[j].isAppRecordRequestHandler) optClassPaths += ""; } break; } } $("#optAddEditRecordDataClassPath").html(optClassPaths); $("#txtAddEditRecordDataData").val(""); }); $("#optAddEditRecordDataClassPath").on("change", function () { if (appsList == null) return; var appName = $("#optAddEditRecordDataAppName").val(); var classPath = $("#optAddEditRecordDataClassPath").val(); for (var i = 0; i < appsList.length; i++) { if (appsList[i].name == appName) { for (var j = 0; j < appsList[i].dnsApps.length; j++) { if (appsList[i].dnsApps[j].classPath == classPath) { $("#txtAddEditRecordDataData").val(appsList[i].dnsApps[j].recordDataTemplate); return; } } } } $("#txtAddEditRecordDataData").val(""); }); $("#optZoneOptionsQuickTsigKeyNames").on("change", function () { var selectedOption = $("#optZoneOptionsQuickTsigKeyNames").val(); switch (selectedOption) { case "blank": break; case "none": $("#txtZoneOptionsZoneTransferTsigKeyNames").val(""); break; default: var existingList = $("#txtZoneOptionsZoneTransferTsigKeyNames").val(); if (existingList.indexOf(selectedOption) < 0) { existingList += selectedOption + "\n"; $("#txtZoneOptionsZoneTransferTsigKeyNames").val(existingList); } break; } }); $("#optZonesPerPage").on("change", function () { localStorage.setItem("optZonesPerPage", $("#optZonesPerPage").val()); }); var optZonesPerPage = localStorage.getItem("optZonesPerPage"); if (optZonesPerPage != null) $("#optZonesPerPage").val(optZonesPerPage); $("#optEditZoneRecordsPerPage").on("change", function () { localStorage.setItem("optEditZoneRecordsPerPage", $("#optEditZoneRecordsPerPage").val()); }); var optEditZoneRecordsPerPage = localStorage.getItem("optEditZoneRecordsPerPage"); if (optEditZoneRecordsPerPage != null) $("#optEditZoneRecordsPerPage").val(optEditZoneRecordsPerPage); $("#chkEditRecordDataSoaUseSerialDateScheme").on("click", function () { var useSerialDateScheme = $("#chkEditRecordDataSoaUseSerialDateScheme").prop("checked"); $("#txtEditRecordDataSoaSerial").prop("disabled", useSerialDateScheme); }); }); function refreshZones(checkDisplay, pageNumber) { if (checkDisplay == null) checkDisplay = false; var divViewZones = $("#divViewZones"); if (checkDisplay) { if (divViewZones.css("display") === "none") return; if (($("#tableZonesBody").html().length > 0) && !$("#mainPanelTabPaneZones").hasClass("active")) return; } if (pageNumber == null) { pageNumber = $("#txtZonesPageNumber").val(); if (pageNumber == "") pageNumber = 1; } var zonesPerPage = Number($("#optZonesPerPage").val()); if (zonesPerPage < 1) zonesPerPage = 10; var node = $("#optZonesClusterNode").val(); var divViewZonesLoader = $("#divViewZonesLoader"); var divEditZone = $("#divEditZone"); divViewZones.hide(); divEditZone.hide(); divViewZonesLoader.show(); HTTPRequest({ url: "api/zones/list?token=" + sessionData.token + "&pageNumber=" + pageNumber + "&zonesPerPage=" + zonesPerPage + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var zones = responseJSON.response.zones; var firstRowNumber = ((responseJSON.response.pageNumber - 1) * zonesPerPage) + 1; var lastRowNumber = firstRowNumber + (zones.length - 1); var tableHtmlRows = ""; for (var i = 0; i < zones.length; i++) { var id = Math.floor(Math.random() * 10000); var name = zones[i].name; if (name === "") name = "."; var type; if (zones[i].internal) { type = "Internal"; } else { switch (zones[i].type) { case "SecondaryForwarder": type = "Secondary Forwarder"; break; case "SecondaryCatalog": type = "Secondary Catalog"; break; default: type = "" + zones[i].type + ""; break; } } var soaSerial = zones[i].soaSerial; if (soaSerial == null) soaSerial = " "; var dnssecStatus = ""; switch (zones[i].dnssecStatus) { case "SignedWithNSEC": case "SignedWithNSEC3": if (zones[i].hasDnssecPrivateKeys) dnssecStatus = "DNSSEC"; else dnssecStatus = "DNSSEC"; break; } var status = ""; if (zones[i].disabled) status = "Disabled"; else if (zones[i].isExpired) status = "Expired"; else if (zones[i].validationFailed) status = "Validation Failed"; else if (zones[i].syncFailed) status = "Sync Failed"; else if (zones[i].notifyFailed) status = "Notify Failed"; else status = "Enabled"; var expiry = zones[i].expiry; if (expiry == null) expiry = " "; else expiry = moment(expiry).local().format("YYYY-MM-DD HH:mm"); var lastModified = zones[i].lastModified; if (lastModified == null) lastModified = " "; else lastModified = moment(lastModified).local().format("YYYY-MM-DD HH:mm"); var isReadOnlyZone = zones[i].internal; var showResyncMenu; switch (zones[i].type) { case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": showResyncMenu = true; break; default: showResyncMenu = false; break; } var hideOptionsMenu; switch (zones[i].type) { case "Primary": hideOptionsMenu = zones[i].internal; break; case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": case "Forwarder": case "Catalog": hideOptionsMenu = false; break; default: hideOptionsMenu = true; break; } var nameTags; if (zones[i].catalog != null) { nameTags = "
    " + htmlEncode(zones[i].catalog) + "
    "; } else { switch (zones[i].type) { case "Catalog": case "SecondaryCatalog": nameTags = "
    " + htmlEncode(name) + "
    "; break; default: nameTags = "
    "; break; } } tableHtmlRows += "" + (firstRowNumber + i) + ""; if (zones[i].nameIdn == null) tableHtmlRows += "" + htmlEncode(name === "." ? "" : name) + "" + nameTags + ""; else tableHtmlRows += "" + htmlEncode(zones[i].nameIdn + " (" + name + ")") + "" + nameTags + ""; tableHtmlRows += "" + type + ""; tableHtmlRows += "" + dnssecStatus + ""; tableHtmlRows += "" + status + ""; tableHtmlRows += "" + soaSerial + ""; tableHtmlRows += "" + expiry + ""; tableHtmlRows += "" + lastModified + ""; tableHtmlRows += "
      "; tableHtmlRows += "
    • " + (isReadOnlyZone ? "View" : "Edit") + " Zone
    • "; if (!zones[i].internal) { tableHtmlRows += "
    • Enable
    • "; tableHtmlRows += "
    • Disable
    • "; } if (showResyncMenu) { tableHtmlRows += "
    • Resync
    • "; } switch (zones[i].type) { case "Primary": case "Forwarder": tableHtmlRows += "
    • Import Zone
    • "; break; } switch (zones[i].type) { case "Primary": case "Forwarder": case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Catalog": tableHtmlRows += "
    • Export Zone
    • "; break; } switch (zones[i].type) { case "Primary": case "Secondary": case "SecondaryForwarder": case "Forwarder": case "SecondaryCatalog": tableHtmlRows += "
    • Convert Zone
    • "; break; } switch (zones[i].type) { case "Primary": case "Forwarder": tableHtmlRows += "
    • Clone Zone
    • "; break; } if (!zones[i].internal) { tableHtmlRows += "
    • Permissions
    • "; } if (!hideOptionsMenu) { tableHtmlRows += "
    • Zone Options
    • "; } if (!zones[i].internal) { tableHtmlRows += "
    • "; tableHtmlRows += "
    • Delete Zone
    • "; } tableHtmlRows += "
    "; } var paginationHtml = ""; if (responseJSON.response.pageNumber > 1) { paginationHtml += "
  • «
  • "; paginationHtml += "
  • "; } var pageStart = responseJSON.response.pageNumber - 5; if (pageStart < 1) pageStart = 1; var pageEnd = pageStart + 9; if (pageEnd > responseJSON.response.totalPages) { var endDiff = pageEnd - responseJSON.response.totalPages; pageEnd = responseJSON.response.totalPages; pageStart -= endDiff; if (pageStart < 1) pageStart = 1; } for (var i = pageStart; i <= pageEnd; i++) { if (i == responseJSON.response.pageNumber) paginationHtml += "
  • " + i + "
  • "; else paginationHtml += "
  • " + i + "
  • "; } if (responseJSON.response.pageNumber < responseJSON.response.totalPages) { paginationHtml += "
  • "; paginationHtml += "
  • »
  • "; } var statusHtml; if (responseJSON.response.zones.length > 0) statusHtml = firstRowNumber + "-" + lastRowNumber + " (" + responseJSON.response.zones.length + ") of " + responseJSON.response.totalZones + " zones (page " + responseJSON.response.pageNumber + " of " + responseJSON.response.totalPages + ")"; else statusHtml = "0 zones"; $("#txtZonesPageNumber").val(responseJSON.response.pageNumber); $("#tableZonesBody").html(tableHtmlRows); $("#tableZonesTopStatus").html(statusHtml); $("#tableZonesTopPagination").html(paginationHtml); $("#tableZonesFooterStatus").html(statusHtml); $("#tableZonesFooterPagination").html(paginationHtml); divViewZonesLoader.hide(); divViewZones.show(); }, error: function () { divViewZonesLoader.hide(); divViewZones.show(); }, invalidToken: function () { divViewZonesLoader.hide(); showPageLogin(); }, objLoaderPlaceholder: divViewZonesLoader }); } function enableZoneMenu(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var zone = mnuItem.attr("data-zone"); var node = $("#optZonesClusterNode").val(); var btn = $("#btnZoneRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/enable?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); $("#mnuEnableZone" + id).hide(); $("#mnuDisableZone" + id).show(); $("#tdZoneStatus" + id).attr("class", "label label-success"); $("#tdZoneStatus" + id).html("Enabled"); showAlert("success", "Zone Enabled!", "Zone '" + zone + "' was enabled successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function enableZone(objBtn) { var zone = $("#titleEditZone").attr("data-zone"); var node = $("#optZonesClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/zones/enable?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#btnEnableZoneEditZone").hide(); $("#btnDisableZoneEditZone").show(); $("#titleEditZoneStatus").attr("class", "label label-success"); $("#titleEditZoneStatus").html("Enabled"); showAlert("success", "Zone Enabled!", "Zone '" + zone + "' was enabled successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function disableZoneMenu(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var zone = mnuItem.attr("data-zone"); if (!confirm("Are you sure you want to disable the zone '" + zone + "'?")) return; var node = $("#optZonesClusterNode").val(); var btn = $("#btnZoneRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/disable?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); $("#mnuEnableZone" + id).show(); $("#mnuDisableZone" + id).hide(); $("#tdZoneStatus" + id).attr("class", "label label-default"); $("#tdZoneStatus" + id).html("Disabled"); showAlert("success", "Zone Disabled!", "Zone '" + zone + "' was disabled successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function disableZone(objBtn) { var zone = $("#titleEditZone").attr("data-zone"); if (!confirm("Are you sure you want to disable the zone '" + zone + "'?")) return; var node = $("#optZonesClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/zones/disable?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#btnEnableZoneEditZone").show(); $("#btnDisableZoneEditZone").hide(); $("#titleEditZoneStatus").attr("class", "label label-default"); $("#titleEditZoneStatus").html("Disabled"); showAlert("success", "Zone Disabled!", "Zone '" + zone + "' was disabled successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function deleteZoneMenu(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var zone = mnuItem.attr("data-zone"); if (!confirm("Are you sure you want to permanently delete the zone '" + zone + "' and all its records?")) return; var node = $("#optZonesClusterNode").val(); var btn = $("#btnZoneRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/delete?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshZones(); showAlert("success", "Zone Deleted!", "Zone '" + zone + "' was deleted successfully."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function deleteZone(objBtn) { var zone = $("#titleEditZone").attr("data-zone"); if (!confirm("Are you sure you want to permanently delete the zone '" + zone + "' and all its records?")) return; var node = $("#optZonesClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/zones/delete?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); refreshZones(); showAlert("success", "Zone Deleted!", "Zone '" + zone + "' was deleted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function showImportZoneModal(zone) { $("#lblImportZoneName").text(zone); $("#divImportZoneAlert").html(""); $("#rdImportZoneTypeFile").prop("checked", true); $("#chkImportZoneOverwrite").prop("checked", true) $("#chkImportZoneOverwriteSoaSerial").prop("checked", false) $("#divImportZoneFile").show(); $("#fileImportZone").val(""); $("#divImportZoneTextEditor").hide(); $("#txtImportZoneText").val(""); $("#btnImportZone").button("reset"); $("#modalImportZone").modal("show"); setTimeout(function () { $("#txtImportZoneText").trigger("focus"); }, 1000); } function importZone() { var divImportZoneAlert = $("#divImportZoneAlert"); var zone = $("#lblImportZoneName").text(); var importType = $("input[name=rdImportZoneType]:checked").val(); var overwrite = $("#chkImportZoneOverwrite").prop("checked"); var overwriteSoaSerial = $("#chkImportZoneOverwriteSoaSerial").prop("checked"); var formData; var contentType; switch (importType) { case "File": var fileImportZone = $("#fileImportZone"); if (fileImportZone[0].files.length === 0) { showAlert("warning", "Missing!", "Please select a zone file to import.", divImportZoneAlert); fileImportZone.trigger("focus"); return; } formData = new FormData(); formData.append("fileImportZone", fileImportZone[0].files[0]); contentType = false; break; default: formData = $("#txtImportZoneText").val(); contentType = "text/plain"; break; } var node = $("#optZonesClusterNode").val(); var btn = $("#btnImportZone"); btn.button("loading"); HTTPRequest({ url: "api/zones/import?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&overwrite=" + overwrite + "&overwriteSoaSerial=" + overwriteSoaSerial + "&node=" + encodeURIComponent(node), method: "POST", data: formData, contentType: contentType, processData: false, success: function (responseJSON) { $("#modalImportZone").modal("hide"); if ($("#divEditZone").is(":visible")) showEditZone(zone); showAlert("success", "Zone Imported!", "The zone file was imported successfully."); }, error: function () { btn.button('reset'); }, invalidToken: function () { $("#modalImportZone").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divImportZoneAlert }); } function exportZone(zone) { var node = $("#optZonesClusterNode").val(); window.open("api/zones/export?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), "_blank"); showAlert("success", "Zone Exported!", "Zone file was exported successfully."); } function showCloneZoneModal(sourceZone) { $("#lblCloneZoneZoneName").text(sourceZone === "." ? "" : sourceZone); $("#divCloneZoneAlert").html(""); $("#txtCloneZoneSourceZoneName").val(sourceZone); $("#txtCloneZoneZoneName").val(""); $("#modalCloneZone").modal("show"); setTimeout(function () { $("#txtCloneZoneZoneName").trigger("focus"); }, 1000); } function cloneZone(objBtn) { var divCloneZoneAlert = $("#divCloneZoneAlert"); var sourceZone = $("#txtCloneZoneSourceZoneName").val(); var zone = $("#txtCloneZoneZoneName").val(); if ((zone == null) || (zone === "")) { showAlert("warning", "Missing!", "Please enter a domain name for the new zone.", divCloneZoneAlert); $("#txtCloneZoneZoneName").trigger("focus"); return; } var node = $("#optZonesClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/zones/clone?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&sourceZone=" + encodeURIComponent(sourceZone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalCloneZone").modal("hide"); if ($("#divEditZone").is(":hidden")) refreshZones(); showAlert("success", "Zone Cloned!", "Zone was cloned from successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalCloneZone").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divCloneZoneAlert }); } function showConvertZoneModal(zone, type) { var lblConvertZoneZoneName = $("#lblConvertZoneZoneName"); lblConvertZoneZoneName.text(zone === "." ? "" : zone); lblConvertZoneZoneName.attr("data-zone", zone); $("#divConvertZoneAlert").html(""); switch (type) { case "Primary": $("#rdConvertZoneToTypePrimary").attr("disabled", true); $("#rdConvertZoneToTypeForwarder").attr("disabled", false); $("#rdConvertZoneToTypeCatalog").attr("disabled", true); $("#rdConvertZoneToTypeForwarder").prop("checked", true); break; case "Secondary": case "SecondaryForwarder": $("#rdConvertZoneToTypePrimary").attr("disabled", false); $("#rdConvertZoneToTypeForwarder").attr("disabled", false); $("#rdConvertZoneToTypeCatalog").attr("disabled", true); $("#rdConvertZoneToTypePrimary").prop("checked", true); break; case "Forwarder": $("#rdConvertZoneToTypePrimary").attr("disabled", false); $("#rdConvertZoneToTypeForwarder").attr("disabled", true); $("#rdConvertZoneToTypeCatalog").attr("disabled", true); $("#rdConvertZoneToTypePrimary").prop("checked", true); break; case "SecondaryCatalog": $("#rdConvertZoneToTypePrimary").attr("disabled", true); $("#rdConvertZoneToTypeForwarder").attr("disabled", true); $("#rdConvertZoneToTypeCatalog").attr("disabled", false); $("#rdConvertZoneToTypeCatalog").prop("checked", true); break; default: $("#rdConvertZoneToTypePrimary").attr("disabled", true); $("#rdConvertZoneToTypeForwarder").attr("disabled", true); $("#rdConvertZoneToTypeCatalog").attr("disabled", true); $("#rdConvertZoneToTypePrimary").prop("checked", false); $("#rdConvertZoneToTypeForwarder").prop("checked", false); $("#rdConvertZoneToTypeCatalog").prop("checked", false); break; } $("#modalConvertZone").modal("show"); } function convertZone(objBtn) { var divConvertZoneAlert = $("#divConvertZoneAlert"); var zone = $("#lblConvertZoneZoneName").attr("data-zone"); var type = $("input[name=rdConvertZoneToType]:checked").val(); var node = $("#optZonesClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/zones/convert?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&type=" + type + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalConvertZone").modal("hide"); if ($("#divEditZone").is(":visible")) showEditZone(zone); else refreshZones(); showAlert("success", "Zone Converted!", "The zone was converted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalConvertZone").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divConvertZoneAlert }); } function addZoneOptionsDynamicUpdatesSecurityPolicyRow(id, tsigKeyName, domain, allowedTypes) { var tbodyDynamicUpdateSecurityPolicy = $("#tbodyDynamicUpdateSecurityPolicy"); if (id == null) { id = Math.floor(Math.random() * 10000); if (tbodyDynamicUpdateSecurityPolicy.is(":empty")) { tsigKeyName = null; domain = $("#lblZoneOptionsZoneName").attr("data-zone"); allowedTypes = 'A,AAAA'.split(','); } } var tableHtmlRow = ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tbodyDynamicUpdateSecurityPolicy.append(tableHtmlRow); } function showZoneOptionsModal(zone) { var divZoneOptionsAlert = $("#divZoneOptionsAlert"); var divZoneOptionsLoader = $("#divZoneOptionsLoader"); var divZoneOptions = $("#divZoneOptions"); $("#lblZoneOptionsZoneName").text(zone === "." ? "" : zone); $("#lblZoneOptionsZoneName").attr("data-zone", zone); divZoneOptionsLoader.show(); divZoneOptions.hide(); var node = $("#optZonesClusterNode").val(); $("#modalZoneOptions").modal("show"); HTTPRequest({ url: "api/zones/options/get?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&includeAvailableCatalogZoneNames=true&includeAvailableTsigKeyNames=true" + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#optZoneOptionsCatalogZoneName").html(""); $("#lblZoneOptionsPrimaryNameServerAddresses").text("Primary Name Server Addresses (Optional)"); $("#divZoneOptionsPrimaryNameServerAddressesInfo").text("Enter the primary name server addresses to sync the zone from. When unspecified, the SOA Primary Name Server will be resolved and used."); $("#txtZoneOptionsPrimaryNameServerAddresses").val(""); $("#rdPrimaryZoneTransferProtocolTcp").prop("checked", true); $("#optZoneOptionsPrimaryZoneTransferTsigKeyName").val(""); $("#chkZoneOptionsValidateZone").prop("checked", false); $("#tabListZoneOptionsGeneral").hide(); $("#divZoneOptionsCatalogNotifyFailedNameServers").hide(); $("#rdDynamicUpdateDeny").prop("checked", true); $("#txtDynamicUpdateNetworkACL").val(""); $("#tbodyDynamicUpdateSecurityPolicy").html(""); $("#txtQueryAccessNetworkACL").prop("disabled", true); $("#txtZoneTransferNetworkACL").prop("disabled", true); $("#txtZoneNotifyNameServers").prop("disabled", true); $("#txtZoneNotifySecondaryCatalogNameServers").prop("disabled", true); $("#txtDynamicUpdateNetworkACL").prop("disabled", true); $("#lblZoneOptionsZoneName").attr("data-zone-type", responseJSON.response.type); //catalog zone switch (responseJSON.response.type) { case "Primary": case "Forwarder": if (responseJSON.response.availableCatalogZoneNames.length > 0) { loadCatalogZoneNamesFrom(responseJSON.response.availableCatalogZoneNames, $("#optZoneOptionsCatalogZoneName"), responseJSON.response.catalog); $("#optZoneOptionsCatalogZoneName").prop("disabled", false); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogQueryAccess); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("checked", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogZoneTransfer); $("#chkZoneOptionsCatalogOverrideNotify").prop("checked", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogNotify); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", (responseJSON.response.catalog == null)); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("disabled", (responseJSON.response.catalog == null)); $("#chkZoneOptionsCatalogOverrideNotify").prop("disabled", (responseJSON.response.catalog == null)); $("#divZoneOptionsCatalogOverrideZoneTransfer").show(); $("#divZoneOptionsCatalogOverrideNotify").show(); $("#divZoneOptionsCatalogOverrideOptions").show(); $("#divZoneOptionsGeneralCatalogZone").show(); $("#tabListZoneOptionsGeneral").show(); } else { $("#divZoneOptionsGeneralCatalogZone").hide(); } break; case "Stub": if ((responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember) { $("#optZoneOptionsCatalogZoneName").html(""); $("#optZoneOptionsCatalogZoneName").prop("disabled", true); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", responseJSON.response.overrideCatalogQueryAccess); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", true); $("#divZoneOptionsCatalogOverrideZoneTransfer").hide(); $("#divZoneOptionsCatalogOverrideNotify").hide(); $("#divZoneOptionsCatalogOverrideOptions").show(); $("#divZoneOptionsGeneralCatalogZone").show(); $("#tabListZoneOptionsGeneral").show(); } else { if (responseJSON.response.availableCatalogZoneNames.length > 0) { loadCatalogZoneNamesFrom(responseJSON.response.availableCatalogZoneNames, $("#optZoneOptionsCatalogZoneName"), responseJSON.response.catalog); $("#optZoneOptionsCatalogZoneName").prop("disabled", false); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogQueryAccess); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", (responseJSON.response.catalog == null)); $("#divZoneOptionsCatalogOverrideZoneTransfer").hide(); $("#divZoneOptionsCatalogOverrideNotify").hide(); $("#divZoneOptionsCatalogOverrideOptions").show(); $("#divZoneOptionsGeneralCatalogZone").show(); $("#tabListZoneOptionsGeneral").show(); } else { $("#divZoneOptionsGeneralCatalogZone").hide(); } } break; case "Secondary": if ((responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember) { $("#optZoneOptionsCatalogZoneName").html(""); $("#optZoneOptionsCatalogZoneName").prop("disabled", true); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", responseJSON.response.overrideCatalogQueryAccess); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("checked", responseJSON.response.overrideCatalogZoneTransfer); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", true); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("disabled", true); $("#divZoneOptionsCatalogOverrideZoneTransfer").show(); $("#divZoneOptionsCatalogOverrideNotify").hide(); $("#divZoneOptionsCatalogOverrideOptions").show(); $("#divZoneOptionsGeneralCatalogZone").show(); $("#tabListZoneOptionsGeneral").show(); } else { if (responseJSON.response.availableCatalogZoneNames.length > 0) { loadCatalogZoneNamesFrom(responseJSON.response.availableCatalogZoneNames, $("#optZoneOptionsCatalogZoneName"), responseJSON.response.catalog); $("#optZoneOptionsCatalogZoneName").prop("disabled", false); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogQueryAccess); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("checked", (responseJSON.response.catalog != null) && responseJSON.response.overrideCatalogZoneTransfer); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", (responseJSON.response.catalog == null)); $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("disabled", (responseJSON.response.catalog == null)); $("#divZoneOptionsCatalogOverrideZoneTransfer").show(); $("#divZoneOptionsCatalogOverrideNotify").hide(); $("#divZoneOptionsCatalogOverrideOptions").show(); $("#divZoneOptionsGeneralCatalogZone").show(); $("#tabListZoneOptionsGeneral").show(); } else { $("#divZoneOptionsGeneralCatalogZone").hide(); } } break; case "SecondaryForwarder": if (responseJSON.response.catalog != null) { $("#optZoneOptionsCatalogZoneName").html(""); $("#optZoneOptionsCatalogZoneName").prop("disabled", true); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked", responseJSON.response.overrideCatalogQueryAccess); $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("disabled", true); $("#divZoneOptionsCatalogOverrideZoneTransfer").hide(); $("#divZoneOptionsCatalogOverrideNotify").hide(); $("#divZoneOptionsCatalogOverrideOptions").show(); $("#divZoneOptionsGeneralCatalogZone").show(); $("#tabListZoneOptionsGeneral").show(); } else { $("#divZoneOptionsGeneralCatalogZone").hide(); } break; default: $("#divZoneOptionsGeneralCatalogZone").hide(); break; } //primary server switch (responseJSON.response.type) { case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": { var value = ""; for (var i = 0; i < responseJSON.response.primaryNameServerAddresses.length; i++) value += responseJSON.response.primaryNameServerAddresses[i] + "\r\n"; $("#txtZoneOptionsPrimaryNameServerAddresses").val(value); } switch (responseJSON.response.primaryZoneTransferProtocol) { case "Tls": $("#rdPrimaryZoneTransferProtocolTls").prop("checked", true); break; case "Quic": $("#rdPrimaryZoneTransferProtocolQuic").prop("checked", true); break; case "Tcp": default: $("#rdPrimaryZoneTransferProtocolTcp").prop("checked", true); break; } loadTsigKeyNamesFrom(responseJSON.response.availableTsigKeyNames, $("#optZoneOptionsPrimaryZoneTransferTsigKeyName"), responseJSON.response.primaryZoneTransferTsigKeyName); if (responseJSON.response.type == "Secondary") { $("#chkZoneOptionsValidateZone").prop("checked", responseJSON.response.validateZone); $("#divZoneOptionsPrimaryServerValidateZone").show(); } else { $("#divZoneOptionsPrimaryServerValidateZone").hide(); } switch (responseJSON.response.type) { case "SecondaryForwarder": case "SecondaryCatalog": $("#lblZoneOptionsPrimaryNameServerAddresses").text("Primary Name Server Addresses"); $("#divZoneOptionsPrimaryNameServerAddressesInfo").text("Enter the primary name server addresses to sync the zone from."); break; } $("#divZoneOptionsPrimaryServerZoneTransferProtocol").show(); $("#divZoneOptionsPrimaryServerZoneTransferTsigKeyName").show(); var disableControls = (responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember; $("#txtZoneOptionsPrimaryNameServerAddresses").prop("disabled", disableControls); $("#rdPrimaryZoneTransferProtocolTcp").prop("disabled", disableControls); $("#rdPrimaryZoneTransferProtocolTls").prop("disabled", disableControls); $("#rdPrimaryZoneTransferProtocolQuic").prop("disabled", disableControls); $("#optZoneOptionsPrimaryZoneTransferTsigKeyName").prop("disabled", disableControls); $("#chkZoneOptionsValidateZone").prop("disabled", disableControls); switch (responseJSON.response.type) { case "Secondary": case "SecondaryForwarder": if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogPrimaryNameServers) { $("#divZoneOptionsGeneralPrimaryServer").show(); $("#tabListZoneOptionsGeneral").show(); } else { $("#divZoneOptionsGeneralPrimaryServer").hide(); } break; default: $("#divZoneOptionsGeneralPrimaryServer").show(); $("#tabListZoneOptionsGeneral").show(); break; } break; case "Stub": { var value = ""; for (var i = 0; i < responseJSON.response.primaryNameServerAddresses.length; i++) value += responseJSON.response.primaryNameServerAddresses[i] + "\r\n"; $("#txtZoneOptionsPrimaryNameServerAddresses").val(value); } $("#txtZoneOptionsPrimaryNameServerAddresses").prop("disabled", (responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember); $("#divZoneOptionsPrimaryServerZoneTransferProtocol").hide(); $("#divZoneOptionsPrimaryServerZoneTransferTsigKeyName").hide(); $("#divZoneOptionsPrimaryServerValidateZone").hide(); $("#divZoneOptionsGeneralPrimaryServer").show(); $("#tabListZoneOptionsGeneral").show(); break; default: $("#divZoneOptionsGeneralPrimaryServer").hide(); break; } //query access { switch (responseJSON.response.queryAccess) { case "Allow": $("#rdQueryAccessAllow").prop("checked", true); break; case "AllowOnlyPrivateNetworks": $("#rdQueryAccessAllowOnlyPrivateNetworks").prop("checked", true); break; case "AllowOnlyZoneNameServers": $("#rdQueryAccessAllowOnlyZoneNameServers").prop("checked", true); break; case "UseSpecifiedNetworkACL": $("#rdQueryAccessUseSpecifiedNetworkACL").prop("checked", true); $("#txtQueryAccessNetworkACL").prop("disabled", false); break; case "AllowZoneNameServersAndUseSpecifiedNetworkACL": $("#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("checked", true); $("#txtQueryAccessNetworkACL").prop("disabled", false); break; case "Deny": default: $("#rdQueryAccessDeny").prop("checked", true); break; } switch (responseJSON.response.type) { case "Stub": case "Forwarder": case "SecondaryForwarder": case "Catalog": case "SecondaryCatalog": $("#divQueryAccessAllowOnlyZoneNameServers").hide(); $("#divQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").hide(); break; default: $("#divQueryAccessAllowOnlyZoneNameServers").show(); $("#divQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").show(); break; } { var value = ""; for (var i = 0; i < responseJSON.response.queryAccessNetworkACL.length; i++) value += responseJSON.response.queryAccessNetworkACL[i] + "\r\n"; $("#txtQueryAccessNetworkACL").val(value); } switch (responseJSON.response.type) { case "Primary": case "Forwarder": case "Catalog": if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogQueryAccess) { $("#rdQueryAccessDeny").prop("disabled", false); $("#rdQueryAccessAllow").prop("disabled", false); $("#rdQueryAccessAllowOnlyPrivateNetworks").prop("disabled", false); $("#rdQueryAccessAllowOnlyZoneNameServers").prop("disabled", false); $("#rdQueryAccessUseSpecifiedNetworkACL").prop("disabled", false); $("#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", false); $("#tabListZoneOptionsQueryAccess").show(); } else { $("#tabListZoneOptionsQueryAccess").hide(); } break; case "Stub": if ((responseJSON.response.catalog != null) && responseJSON.response.isSecondaryCatalogMember) { if (responseJSON.response.overrideCatalogQueryAccess) { $("#rdQueryAccessDeny").prop("disabled", true); $("#rdQueryAccessAllow").prop("disabled", true); $("#rdQueryAccessAllowOnlyPrivateNetworks").prop("disabled", true); $("#rdQueryAccessAllowOnlyZoneNameServers").prop("disabled", true); $("#rdQueryAccessUseSpecifiedNetworkACL").prop("disabled", true); $("#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", true); $("#txtQueryAccessNetworkACL").prop("disabled", true); $("#tabListZoneOptionsQueryAccess").show(); } else { $("#tabListZoneOptionsQueryAccess").hide(); } } else { if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogQueryAccess) { $("#rdQueryAccessDeny").prop("disabled", false); $("#rdQueryAccessAllow").prop("disabled", false); $("#rdQueryAccessAllowOnlyPrivateNetworks").prop("disabled", false); $("#rdQueryAccessAllowOnlyZoneNameServers").prop("disabled", false); $("#rdQueryAccessUseSpecifiedNetworkACL").prop("disabled", false); $("#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", false); $("#tabListZoneOptionsQueryAccess").show(); } else { $("#tabListZoneOptionsQueryAccess").hide(); } } break; case "Secondary": case "SecondaryForwarder": if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogQueryAccess) { $("#rdQueryAccessDeny").prop("disabled", responseJSON.response.catalog != null); $("#rdQueryAccessAllow").prop("disabled", responseJSON.response.catalog != null); $("#rdQueryAccessAllowOnlyPrivateNetworks").prop("disabled", responseJSON.response.catalog != null); $("#rdQueryAccessAllowOnlyZoneNameServers").prop("disabled", responseJSON.response.catalog != null); $("#rdQueryAccessUseSpecifiedNetworkACL").prop("disabled", responseJSON.response.catalog != null); $("#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", responseJSON.response.catalog != null); if (responseJSON.response.catalog != null) $("#txtQueryAccessNetworkACL").prop("disabled", true); $("#tabListZoneOptionsQueryAccess").show(); } else { $("#tabListZoneOptionsQueryAccess").hide(); } break; case "SecondaryCatalog": $("#rdQueryAccessDeny").prop("disabled", true); $("#rdQueryAccessAllow").prop("disabled", true); $("#rdQueryAccessAllowOnlyPrivateNetworks").prop("disabled", true); $("#rdQueryAccessAllowOnlyZoneNameServers").prop("disabled", true); $("#rdQueryAccessUseSpecifiedNetworkACL").prop("disabled", true); $("#rdQueryAccessAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", true); $("#txtQueryAccessNetworkACL").prop("disabled", true); $("#tabListZoneOptionsQueryAccess").show(); break; default: $("#tabListZoneOptionsQueryAccess").hide(); break; } } //zone transfer switch (responseJSON.response.type) { case "Primary": case "Secondary": case "Forwarder": case "Catalog": case "SecondaryCatalog": switch (responseJSON.response.zoneTransfer) { case "Allow": $("#rdZoneTransferAllow").prop("checked", true); break; case "AllowOnlyZoneNameServers": $("#rdZoneTransferAllowOnlyZoneNameServers").prop("checked", true); break; case "UseSpecifiedNetworkACL": $("#rdZoneTransferUseSpecifiedNetworkACL").prop("checked", true); $("#txtZoneTransferNetworkACL").prop("disabled", false); break; case "AllowZoneNameServersAndUseSpecifiedNetworkACL": $("#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("checked", true); $("#txtZoneTransferNetworkACL").prop("disabled", false); break; case "Deny": default: $("#rdZoneTransferDeny").prop("checked", true); break; } { var value = ""; for (var i = 0; i < responseJSON.response.zoneTransferNetworkACL.length; i++) value += responseJSON.response.zoneTransferNetworkACL[i] + "\r\n"; $("#txtZoneTransferNetworkACL").val(value); } { var value = ""; if (responseJSON.response.zoneTransferTsigKeyNames != null) { for (var i = 0; i < responseJSON.response.zoneTransferTsigKeyNames.length; i++) { value += responseJSON.response.zoneTransferTsigKeyNames[i] + "\r\n"; } } $("#txtZoneOptionsZoneTransferTsigKeyNames").val(value); } { var options = ""; if (responseJSON.response.availableTsigKeyNames != null) { for (var i = 0; i < responseJSON.response.availableTsigKeyNames.length; i++) { options += ""; } } $("#optZoneOptionsQuickTsigKeyNames").html(options); } switch (responseJSON.response.type) { case "Forwarder": case "Catalog": case "SecondaryCatalog": $("#divZoneTransferAllowOnlyZoneNameServers").hide(); $("#divZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").hide(); break; default: $("#divZoneTransferAllowOnlyZoneNameServers").show(); $("#divZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").show(); break; } switch (responseJSON.response.type) { case "Primary": case "Forwarder": if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogZoneTransfer) { $("#rdZoneTransferDeny").prop("disabled", false); $("#rdZoneTransferAllow").prop("disabled", false); $("#rdZoneTransferAllowOnlyZoneNameServers").prop("disabled", false); $("#rdZoneTransferUseSpecifiedNetworkACL").prop("disabled", false); $("#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", false); $("#txtZoneOptionsZoneTransferTsigKeyNames").prop("disabled", false); $("#optZoneOptionsQuickTsigKeyNames").prop("disabled", false); $("#tabListZoneOptionsZoneTranfer").show(); } else { $("#tabListZoneOptionsZoneTranfer").hide(); } break; case "Secondary": if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogZoneTransfer) { $("#rdZoneTransferDeny").prop("disabled", responseJSON.response.catalog != null); $("#rdZoneTransferAllow").prop("disabled", responseJSON.response.catalog != null); $("#rdZoneTransferAllowOnlyZoneNameServers").prop("disabled", responseJSON.response.catalog != null); $("#rdZoneTransferUseSpecifiedNetworkACL").prop("disabled", responseJSON.response.catalog != null); $("#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", responseJSON.response.catalog != null); if (responseJSON.response.catalog != null) $("#txtZoneTransferNetworkACL").prop("disabled", true); $("#txtZoneOptionsZoneTransferTsigKeyNames").prop("disabled", responseJSON.response.catalog != null); $("#optZoneOptionsQuickTsigKeyNames").prop("disabled", responseJSON.response.catalog != null); $("#tabListZoneOptionsZoneTranfer").show(); } else { $("#tabListZoneOptionsZoneTranfer").hide(); } break; case "Catalog": $("#rdZoneTransferDeny").prop("disabled", false); $("#rdZoneTransferAllow").prop("disabled", false); $("#rdZoneTransferAllowOnlyZoneNameServers").prop("disabled", false); $("#rdZoneTransferUseSpecifiedNetworkACL").prop("disabled", false); $("#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", false); $("#txtZoneOptionsZoneTransferTsigKeyNames").prop("disabled", false); $("#optZoneOptionsQuickTsigKeyNames").prop("disabled", false); $("#tabListZoneOptionsZoneTranfer").show(); break; case "SecondaryCatalog": $("#rdZoneTransferDeny").prop("disabled", true); $("#rdZoneTransferAllow").prop("disabled", true); $("#rdZoneTransferAllowOnlyZoneNameServers").prop("disabled", true); $("#rdZoneTransferUseSpecifiedNetworkACL").prop("disabled", true); $("#rdZoneTransferAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("disabled", true); $("#txtZoneTransferNetworkACL").prop("disabled", true); $("#txtZoneOptionsZoneTransferTsigKeyNames").prop("disabled", true); $("#optZoneOptionsQuickTsigKeyNames").prop("disabled", true); $("#tabListZoneOptionsZoneTranfer").show(); break; } break; default: $("#tabListZoneOptionsZoneTranfer").hide(); break; } //notify switch (responseJSON.response.type) { case "Primary": case "Secondary": case "Forwarder": case "Catalog": switch (responseJSON.response.notify) { case "ZoneNameServers": $("#rdZoneNotifyZoneNameServers").prop("checked", true); break; case "SpecifiedNameServers": $("#rdZoneNotifySpecifiedNameServers").prop("checked", true); $("#txtZoneNotifyNameServers").prop("disabled", false); break; case "BothZoneAndSpecifiedNameServers": $("#rdZoneNotifyBothZoneAndSpecifiedNameServers").prop("checked", true); $("#txtZoneNotifyNameServers").prop("disabled", false); break; case "SeparateNameServersForCatalogAndMemberZones": $("#rdZoneNotifySeparateNameServersForCatalogAndMemberZones").prop("checked", true); $("#txtZoneNotifyNameServers").prop("disabled", false); $("#txtZoneNotifySecondaryCatalogNameServers").prop("disabled", false); break; case "None": default: $("#rdZoneNotifyNone").prop("checked", true); break; } { var value = ""; for (var i = 0; i < responseJSON.response.notifyNameServers.length; i++) value += responseJSON.response.notifyNameServers[i] + "\r\n"; $("#txtZoneNotifyNameServers").val(value); } if (responseJSON.response.notifySecondaryCatalogsNameServers != null) { var value = ""; for (var i = 0; i < responseJSON.response.notifySecondaryCatalogsNameServers.length; i++) value += responseJSON.response.notifySecondaryCatalogsNameServers[i] + "\r\n"; $("#txtZoneNotifySecondaryCatalogNameServers").val(value); } else { $("#txtZoneNotifySecondaryCatalogNameServers").val(""); } if (responseJSON.response.notifyFailed) { var value = ""; for (var i = 0; i < responseJSON.response.notifyFailedFor.length; i++) { if (i == 0) value = responseJSON.response.notifyFailedFor[i]; else value += ", " + responseJSON.response.notifyFailedFor[i]; } if ((responseJSON.response.catalog != null) && !responseJSON.response.overrideCatalogNotify) { $("#divZoneOptionsCatalogNotifyFailedNameServers").show(); $("#lblZoneOptionsCatalogNotifyFailedNameServers").text(value); } $("#divZoneNotifyFailedNameServers").show(); $("#lblZoneNotifyFailedNameServers").text(value); } else { $("#divZoneNotifyFailedNameServers").hide(); } switch (responseJSON.response.type) { case "Forwarder": $("#divZoneNotifyZoneNameServers").hide(); $("#divZoneNotifyBothZoneAndSpecifiedNameServers").hide(); $("#divZoneNotifySeparateNameServersForCatalogAndMemberZones").hide(); $("#divZoneNotifySecondaryCatalogNameServers").hide(); break; case "Catalog": $("#divZoneNotifyZoneNameServers").hide(); $("#divZoneNotifyBothZoneAndSpecifiedNameServers").hide(); $("#divZoneNotifySeparateNameServersForCatalogAndMemberZones").show(); $("#divZoneNotifySecondaryCatalogNameServers").show(); break; default: $("#divZoneNotifyZoneNameServers").show(); $("#divZoneNotifyBothZoneAndSpecifiedNameServers").show(); $("#divZoneNotifySeparateNameServersForCatalogAndMemberZones").hide(); $("#divZoneNotifySecondaryCatalogNameServers").hide(); break; } switch (responseJSON.response.type) { case "Primary": case "Forwarder": if ((responseJSON.response.catalog == null) || responseJSON.response.overrideCatalogNotify) $("#tabListZoneOptionsNotify").show(); else $("#tabListZoneOptionsNotify").hide(); break; case "Secondary": case "Catalog": $("#tabListZoneOptionsNotify").show(); break; } break; default: $("#tabListZoneOptionsNotify").hide(); break; } //dynamic update switch (responseJSON.response.type) { case "Primary": case "Secondary": case "SecondaryForwarder": case "Forwarder": //dynamic update switch (responseJSON.response.update) { case "Allow": $("#rdDynamicUpdateAllow").prop("checked", true); break; case "AllowOnlyZoneNameServers": $("#rdDynamicUpdateAllowOnlyZoneNameServers").prop("checked", true); break; case "UseSpecifiedNetworkACL": $("#rdDynamicUpdateUseSpecifiedNetworkACL").prop("checked", true); $("#txtDynamicUpdateNetworkACL").prop("disabled", false); break; case "AllowZoneNameServersAndUseSpecifiedNetworkACL": $("#rdDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL").prop("checked", true); $("#txtDynamicUpdateNetworkACL").prop("disabled", false); break; case "Deny": default: $("#rdDynamicUpdateDeny").prop("checked", true); break; } { var value = ""; for (var i = 0; i < responseJSON.response.updateNetworkACL.length; i++) value += responseJSON.response.updateNetworkACL[i] + "\r\n"; $("#txtDynamicUpdateNetworkACL").val(value); } $("#tbodyDynamicUpdateSecurityPolicy").html(""); switch (responseJSON.response.type) { case "Primary": case "Forwarder": zoneOptionsAvailableTsigKeyNames = responseJSON.response.availableTsigKeyNames; if (responseJSON.response.updateSecurityPolicies != null) { for (var i = 0; i < responseJSON.response.updateSecurityPolicies.length; i++) addZoneOptionsDynamicUpdatesSecurityPolicyRow(i, responseJSON.response.updateSecurityPolicies[i].tsigKeyName, responseJSON.response.updateSecurityPolicies[i].domain, responseJSON.response.updateSecurityPolicies[i].allowedTypes); } $("#divDynamicUpdateSecurityPolicy").show(); break; default: $("#divDynamicUpdateSecurityPolicy").hide(); break; } switch (responseJSON.response.type) { case "Secondary": case "SecondaryForwarder": case "Forwarder": $("#divDynamicUpdateAllowOnlyZoneNameServers").hide(); $("#divDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL").hide(); break; default: $("#divDynamicUpdateAllowOnlyZoneNameServers").show(); $("#divDynamicUpdateAllowZoneNameServersAndUseSpecifiedNetworkACL").show(); break; } $("#tabListZoneOptionsUpdate").show(); break; default: $("#tabListZoneOptionsUpdate").hide(); break; } //tab focus switch (responseJSON.response.type) { case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": $("#tabListZoneOptionsGeneral").addClass("active"); $("#tabPaneZoneOptionsGeneral").addClass("active"); $("#tabListZoneOptionsQueryAccess").removeClass("active"); $("#tabPaneZoneOptionsQueryAccess").removeClass("active"); $("#tabListZoneOptionsZoneTranfer").removeClass("active"); $("#tabPaneZoneOptionsZoneTransfer").removeClass("active"); $("#tabListZoneOptionsNotify").removeClass("active"); $("#tabPaneZoneOptionsNotify").removeClass("active"); $("#tabListZoneOptionsUpdate").removeClass("active"); $("#tabPaneZoneOptionsUpdate").removeClass("active"); break; case "Catalog": $("#tabListZoneOptionsGeneral").removeClass("active"); $("#tabPaneZoneOptionsGeneral").removeClass("active"); $("#tabListZoneOptionsQueryAccess").addClass("active"); $("#tabPaneZoneOptionsQueryAccess").addClass("active"); $("#tabListZoneOptionsZoneTranfer").removeClass("active"); $("#tabPaneZoneOptionsZoneTransfer").removeClass("active"); $("#tabListZoneOptionsNotify").removeClass("active"); $("#tabPaneZoneOptionsNotify").removeClass("active"); $("#tabListZoneOptionsUpdate").removeClass("active"); $("#tabPaneZoneOptionsUpdate").removeClass("active"); break; case "Primary": case "Forwarder": if (responseJSON.response.availableCatalogZoneNames.length > 0) { $("#tabListZoneOptionsGeneral").addClass("active"); $("#tabPaneZoneOptionsGeneral").addClass("active"); $("#tabListZoneOptionsQueryAccess").removeClass("active"); $("#tabPaneZoneOptionsQueryAccess").removeClass("active"); $("#tabListZoneOptionsZoneTranfer").removeClass("active"); $("#tabPaneZoneOptionsZoneTransfer").removeClass("active"); $("#tabListZoneOptionsNotify").removeClass("active"); $("#tabPaneZoneOptionsNotify").removeClass("active"); $("#tabListZoneOptionsUpdate").removeClass("active"); $("#tabPaneZoneOptionsUpdate").removeClass("active"); } else { $("#tabListZoneOptionsGeneral").removeClass("active"); $("#tabPaneZoneOptionsGeneral").removeClass("active"); $("#tabListZoneOptionsQueryAccess").addClass("active"); $("#tabPaneZoneOptionsQueryAccess").addClass("active"); $("#tabListZoneOptionsZoneTranfer").removeClass("active"); $("#tabPaneZoneOptionsZoneTransfer").removeClass("active"); $("#tabListZoneOptionsNotify").removeClass("active"); $("#tabPaneZoneOptionsNotify").removeClass("active"); $("#tabListZoneOptionsUpdate").removeClass("active"); $("#tabPaneZoneOptionsUpdate").removeClass("active"); } break; } divZoneOptionsLoader.hide(); divZoneOptions.show(); }, error: function () { divZoneOptionsLoader.hide(); }, invalidToken: function () { $("#modalZoneOptions").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divZoneOptionsAlert, objLoaderPlaceholder: divZoneOptionsLoader }); } function saveZoneOptions() { var divZoneOptionsAlert = $("#divZoneOptionsAlert"); var divZoneOptionsLoader = $("#divZoneOptionsLoader"); var zone = $("#lblZoneOptionsZoneName").attr("data-zone"); var zoneType = $("#lblZoneOptionsZoneName").attr("data-zone-type"); //general catalog zone name var catalog = $("#optZoneOptionsCatalogZoneName").val(); if (catalog == null) catalog = ""; var overrideCatalogQueryAccess = $("#chkZoneOptionsCatalogOverrideQueryAccess").prop("checked"); var overrideCatalogZoneTransfer = $("#chkZoneOptionsCatalogOverrideZoneTransfer").prop("checked"); var overrideCatalogNotify = $("#chkZoneOptionsCatalogOverrideNotify").prop("checked"); //general primary name server for secondary & stub var primaryNameServerAddresses = cleanTextList($("#txtZoneOptionsPrimaryNameServerAddresses").val()); switch (zoneType) { case "SecondaryForwarder": case "SecondaryCatalog": if ((primaryNameServerAddresses.length === 0) || (primaryNameServerAddresses === ",")) { showAlert("warning", "Missing!", "Please enter at least one primary name server address to proceed.", divZoneOptionsAlert); $("#txtZoneOptionsPrimaryNameServerAddresses").trigger("focus"); return; } break; } var primaryZoneTransferProtocol = $("input[name=rdPrimaryZoneTransferProtocol]:checked").val(); var primaryZoneTransferTsigKeyName = $("#optZoneOptionsPrimaryZoneTransferTsigKeyName").val(); var validateZone = $("#chkZoneOptionsValidateZone").prop("checked"); //query access var queryAccess = $("input[name=rdQueryAccess]:checked").val(); var queryAccessNetworkACL = cleanTextList($("#txtQueryAccessNetworkACL").val()); //zone transfer var zoneTransfer = $("input[name=rdZoneTransfer]:checked").val(); var zoneTransferNetworkACL = cleanTextList($("#txtZoneTransferNetworkACL").val()); if ((zoneTransferNetworkACL.length === 0) || (zoneTransferNetworkACL === ",")) zoneTransferNetworkACL = false; else $("#txtZoneTransferNetworkACL").val(zoneTransferNetworkACL.replace(/,/g, "\n")); var zoneTransferTsigKeyNames = cleanTextList($("#txtZoneOptionsZoneTransferTsigKeyNames").val()); if ((zoneTransferTsigKeyNames.length === 0) || (zoneTransferTsigKeyNames === ",")) zoneTransferTsigKeyNames = false; else $("#txtZoneOptionsZoneTransferTsigKeyNames").val(zoneTransferTsigKeyNames.replace(/,/g, "\n")); //notify var notify = $("input[name=rdZoneNotify]:checked").val(); var notifyNameServers = cleanTextList($("#txtZoneNotifyNameServers").val()); if ((notifyNameServers.length === 0) || (notifyNameServers === ",")) notifyNameServers = false; else $("#txtZoneNotifyNameServers").val(notifyNameServers.replace(/,/g, "\n")); var notifySecondaryCatalogsNameServers = cleanTextList($("#txtZoneNotifySecondaryCatalogNameServers").val()); if ((notifySecondaryCatalogsNameServers.length === 0) || (notifySecondaryCatalogsNameServers === ",")) notifySecondaryCatalogsNameServers = false; else $("#txtZoneNotifySecondaryCatalogNameServers").val(notifySecondaryCatalogsNameServers.replace(/,/g, "\n")); //dynamic update var update = $("input[name=rdDynamicUpdate]:checked").val(); var updateNetworkACL = cleanTextList($("#txtDynamicUpdateNetworkACL").val()); if ((updateNetworkACL.length === 0) || (updateNetworkACL === ",")) updateNetworkACL = false; else $("#txtDynamicUpdateNetworkACL").val(updateNetworkACL.replace(/,/g, "\n")); var updateSecurityPolicies = serializeTableData($("#tableDynamicUpdateSecurityPolicy"), 3, divZoneOptionsAlert); if (updateSecurityPolicies === false) return; if (updateSecurityPolicies.length === 0) updateSecurityPolicies = false; var node = $("#optZonesClusterNode").val(); var btn = $("#btnSaveZoneOptions"); btn.button("loading"); HTTPRequest({ url: "api/zones/options/set?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&catalog=" + encodeURIComponent(catalog) + "&overrideCatalogQueryAccess=" + overrideCatalogQueryAccess + "&overrideCatalogZoneTransfer=" + overrideCatalogZoneTransfer + "&overrideCatalogNotify=" + overrideCatalogNotify + "&primaryNameServerAddresses=" + encodeURIComponent(primaryNameServerAddresses) + "&primaryZoneTransferProtocol=" + primaryZoneTransferProtocol + "&primaryZoneTransferTsigKeyName=" + encodeURIComponent(primaryZoneTransferTsigKeyName) + "&validateZone=" + validateZone + "&queryAccess=" + queryAccess + "&queryAccessNetworkACL=" + encodeURIComponent(queryAccessNetworkACL) + "&zoneTransfer=" + zoneTransfer + "&zoneTransferNetworkACL=" + encodeURIComponent(zoneTransferNetworkACL) + "&zoneTransferTsigKeyNames=" + encodeURIComponent(zoneTransferTsigKeyNames) + "¬ify=" + notify + "¬ifyNameServers=" + encodeURIComponent(notifyNameServers) + "¬ifySecondaryCatalogsNameServers=" + encodeURIComponent(notifySecondaryCatalogsNameServers) + "&update=" + update + "&updateNetworkACL=" + encodeURIComponent(updateNetworkACL) + "&updateSecurityPolicies=" + encodeURIComponent(updateSecurityPolicies) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalZoneOptions").modal("hide"); var zonesRowId = $("#btnSaveZoneOptions").attr("data-zones-row-id"); if (zonesRowId == null) { switch (zoneType) { case "Catalog": case "SecondaryCatalog": break; default: if ((catalog == null) || (catalog == "")) { $("#titleEditZoneCatalog").hide(); $("#titleEditZoneCatalog").text(""); } else { $("#titleEditZoneCatalog").attr("class", "label label-default"); $("#titleEditZoneCatalog").text(catalog); $("#titleEditZoneCatalog").show(); } break; } } else { switch (zoneType) { case "Catalog": case "SecondaryCatalog": break; default: if ((catalog == null) || (catalog == "")) { $("#tagZoneCatalogName" + zonesRowId).hide(); } else { $("#tagZoneCatalogName" + zonesRowId).text(catalog); $("#tagZoneCatalogName" + zonesRowId).show(); } break; } } showAlert("success", "Options Saved!", "Zone options were saved successfully."); }, error: function () { btn.button("reset"); divZoneOptionsLoader.hide(); }, invalidToken: function () { btn.button("reset"); $("#modalZoneOptions").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divZoneOptionsAlert, objLoaderPlaceholder: divZoneOptionsLoader }); } function showZonePermissionsModal(zone) { var divEditPermissionsAlert = $("#divEditPermissionsAlert"); var divEditPermissionsLoader = $("#divEditPermissionsLoader"); var divEditPermissionsViewer = $("#divEditPermissionsViewer"); $("#lblEditPermissionsName").text("Zones / " + (zone === "." ? "" : zone)); $("#tbodyEditPermissionsUser").html(""); $("#tbodyEditPermissionsGroup").html(""); divEditPermissionsLoader.show(); divEditPermissionsViewer.hide(); var btnEditPermissionsSave = $("#btnEditPermissionsSave"); btnEditPermissionsSave.attr("onclick", "saveZonePermissions(this); return false;"); btnEditPermissionsSave.show(); var node = $("#optZonesClusterNode").val(); var modalEditPermissions = $("#modalEditPermissions"); modalEditPermissions.modal("show"); HTTPRequest({ url: "api/zones/permissions/get?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&includeUsersAndGroups=true" + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#lblEditPermissionsName").text(responseJSON.response.section + " / " + (responseJSON.response.subItem == "." ? "" : responseJSON.response.subItem)); //user permissions for (var i = 0; i < responseJSON.response.userPermissions.length; i++) { addEditPermissionUserRow(i, responseJSON.response.userPermissions[i].username, responseJSON.response.userPermissions[i].canView, responseJSON.response.userPermissions[i].canModify, responseJSON.response.userPermissions[i].canDelete); } //load users list var userListHtml = ""; for (var i = 0; i < responseJSON.response.users.length; i++) { userListHtml += ""; } $("#optEditPermissionsUserList").html(userListHtml); //group permissions for (var i = 0; i < responseJSON.response.groupPermissions.length; i++) { addEditPermissionGroupRow(i, responseJSON.response.groupPermissions[i].name, responseJSON.response.groupPermissions[i].canView, responseJSON.response.groupPermissions[i].canModify, responseJSON.response.groupPermissions[i].canDelete); } //load groups list var groupListHtml = ""; for (var i = 0; i < responseJSON.response.groups.length; i++) { groupListHtml += ""; } $("#optEditPermissionsGroupList").html(groupListHtml); btnEditPermissionsSave.attr("data-zone", responseJSON.response.subItem); divEditPermissionsLoader.hide(); divEditPermissionsViewer.show(); }, error: function () { divEditPermissionsLoader.hide(); }, invalidToken: function () { modalEditPermissions.modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditPermissionsAlert, objLoaderPlaceholder: divEditPermissionsLoader }); } function saveZonePermissions(objBtn) { var btn = $(objBtn); var divEditPermissionsAlert = $("#divEditPermissionsAlert"); var zone = btn.attr("data-zone"); var userPermissions = serializeTableData($("#tableEditPermissionsUser"), 4); var groupPermissions = serializeTableData($("#tableEditPermissionsGroup"), 4); var node = $("#optZonesClusterNode").val(); var apiUrl = "api/zones/permissions/set?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&userPermissions=" + encodeURIComponent(userPermissions) + "&groupPermissions=" + encodeURIComponent(groupPermissions); btn.button("loading"); HTTPRequest({ url: apiUrl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalEditPermissions").modal("hide"); showAlert("success", "Permissions Saved!", "Zone permissions were saved successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalEditPermissions").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divEditPermissionsAlert }); } function resyncZoneMenu(objMenuItem) { var mnuItem = $(objMenuItem); var id = mnuItem.attr("data-id"); var zone = mnuItem.attr("data-zone"); var zoneType = mnuItem.attr("data-zone-type"); if (zoneType == "Secondary") { if (!confirm("The resync action will perform a full zone transfer (AXFR). You will need to check the logs to confirm if the resync action was successful.\r\n\r\nAre you sure you want to resync the '" + zone + "' zone?")) return; } else { if (!confirm("The resync action will perform a full zone refresh. You will need to check the logs to confirm if the resync action was successful.\r\n\r\nAre you sure you want to resync the '" + zone + "' zone?")) return; } var node = $("#optZonesClusterNode").val(); var btn = $("#btnZoneRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/resync?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.prop("disabled", false); btn.html(originalBtnHtml); showAlert("success", "Resync Triggered!", "Zone '" + zone + "' resync was triggered successfully. Please check the Logs for confirmation."); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { showPageLogin(); } }); } function resyncZone(objBtn, zone) { if ($("#titleEditZoneType").text() == "Secondary") { if (!confirm("The resync action will perform a full zone transfer (AXFR). You will need to check the logs to confirm if the resync action was successful.\r\n\r\nAre you sure you want to resync the '" + zone + "' zone?")) return; } else { if (!confirm("The resync action will perform a full zone refresh. You will need to check the logs to confirm if the resync action was successful.\r\n\r\nAre you sure you want to resync the '" + zone + "' zone?")) return; } var node = $("#optZonesClusterNode").val(); var btn = $(objBtn); btn.button("loading"); HTTPRequest({ url: "api/zones/resync?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); showAlert("success", "Resync Triggered!", "Zone '" + zone + "' resync was triggered successfully. Please check the Logs for confirmation."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); showPageLogin(); } }); } function showAddZoneModal() { $("#divAddZoneAlert").html(""); $("#txtAddZone").val(""); $("#txtAddZone").prop("disabled", false); $("#rdAddZoneTypePrimary").prop("checked", true); $("#chkAddZoneInitializeForwarder").prop("checked", true); $("#fileAddZoneImportZone").val(""); $("#chkAddZoneUseSoaSerialDateScheme").prop("checked", $("#chkUseSoaSerialDateScheme").prop("checked")); $("#txtAddZonePrimaryNameServerAddresses").val(""); $("#rdAddZoneZoneTransferProtocolTcp").prop("checked", true); $("#optAddZoneTsigKeyName").val(""); $("#chkAddZoneValidateZone").prop("checked", false); $("input[name=rdAddZoneForwarderProtocol]:radio").attr("disabled", false); $("#rdAddZoneForwarderProtocolUdp").prop("checked", true); $("#chkAddZoneForwarderThisServer").prop("checked", false); $("#txtAddZoneForwarder").prop("disabled", false); $("#txtAddZoneForwarder").attr("placeholder", "8.8.8.8 or [2620:fe::10]") $("#txtAddZoneForwarder").val(""); $("#chkAddZoneForwarderDnssecValidation").prop("checked", $("#chkDnssecValidation").prop("checked")); $("#rdAddZoneForwarderProxyTypeDefaultProxy").prop("checked", true); $("#txtAddZoneForwarderProxyAddress").prop("disabled", true); $("#txtAddZoneForwarderProxyPort").prop("disabled", true); $("#txtAddZoneForwarderProxyUsername").prop("disabled", true); $("#txtAddZoneForwarderProxyPassword").prop("disabled", true); $("#txtAddZoneForwarderProxyAddress").val(""); $("#txtAddZoneForwarderProxyPort").val(""); $("#txtAddZoneForwarderProxyUsername").val(""); $("#txtAddZoneForwarderProxyPassword").val(""); $("#divAddZoneCatalogZone").hide(); $("#divAddZoneInitializeForwarder").hide(); $("#divAddZoneImportZoneFile").show(); $("#divAddZoneUseSoaSerialDateScheme").show(); $("#divAddZonePrimaryNameServerAddresses").hide(); $("#divAddZoneZoneTransferProtocol").hide(); $("#divAddZoneTsigKeyName").hide(); $("#divAddZoneValidateZone").hide(); $("#divAddZoneForwarderProtocol").hide(); $("#divAddZoneForwarder").hide(); $("#divAddZoneForwarderDnssecValidation").hide(); $("#divAddZoneForwarderProxy").hide(); $("#btnAddZone").button('reset'); $("#modalAddZone").modal("show"); setTimeout(function () { $("#txtAddZone").trigger("focus"); }, 1000); var currentValue = null; if (sessionData.info.clusterInitialized) currentValue = "cluster-catalog." + sessionData.info.clusterDomain; loadCatalogZoneNames($("#optAddZoneCatalogZoneName"), currentValue, $("#divAddZoneAlert"), $("#divAddZoneCatalogZone")); } function loadCatalogZoneNames(jqDropDown, currentValue, divAlertPlaceholder, divCatalogZone) { jqDropDown.prop("disabled", true); jqDropDown.attr("hasItems", false); if (currentValue == null) currentValue = ""; if (currentValue.length == 0) { jqDropDown.html(""); } else { jqDropDown.html(""); jqDropDown.val(currentValue); } var node = $("#optZonesClusterNode").val(); HTTPRequest({ url: "api/zones/catalogs/list?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { loadCatalogZoneNamesFrom(responseJSON.response.catalogZoneNames, jqDropDown, currentValue); if ((divCatalogZone != null) && (responseJSON.response.catalogZoneNames.length > 0)) divCatalogZone.show(); }, error: function () { jqDropDown.prop("disabled", false); }, invalidToken: function () { jqDropDown.prop("disabled", false); showPageLogin(); }, objAlertPlaceholder: divAlertPlaceholder }); } function loadCatalogZoneNamesFrom(catalogZoneNames, jqDropDown, currentValue) { var optionsHtml; if ((currentValue == null) || (currentValue.length == 0)) optionsHtml = ""; else optionsHtml = ""; for (var i = 0; i < catalogZoneNames.length; i++) { optionsHtml += "" + htmlEncode(catalogZoneNames[i]) + ""; } jqDropDown.html(optionsHtml); jqDropDown.prop("disabled", false); jqDropDown.attr("hasItems", catalogZoneNames.length > 0); } function loadTsigKeyNames(jqDropDown, currentValue, divAlertPlaceholder) { jqDropDown.prop("disabled", true); if (currentValue == null) currentValue = ""; if (currentValue.length == 0) { jqDropDown.html(""); } else { jqDropDown.html(""); jqDropDown.val(currentValue); } var node = $("#optZonesClusterNode").val(); HTTPRequest({ url: "api/settings/getTsigKeyNames?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { loadTsigKeyNamesFrom(responseJSON.response.tsigKeyNames, jqDropDown, currentValue); }, error: function () { jqDropDown.prop("disabled", false); }, invalidToken: function () { jqDropDown.prop("disabled", false); showPageLogin(); }, objAlertPlaceholder: divAlertPlaceholder }); } function loadTsigKeyNamesFrom(tsigKeyNames, jqDropDown, currentValue) { var optionsHtml; if ((currentValue == null) || (currentValue.length == 0)) optionsHtml = ""; else optionsHtml = ""; for (var i = 0; i < tsigKeyNames.length; i++) { optionsHtml += "" + htmlEncode(tsigKeyNames[i]) + ""; } jqDropDown.html(optionsHtml); jqDropDown.prop("disabled", false); } function updateAddZoneFormForwarderThisServer() { var useThisServer = $("#chkAddZoneForwarderThisServer").prop('checked'); if (useThisServer) { $("input[name=rdAddZoneForwarderProtocol]:radio").attr("disabled", true); $("#rdAddZoneForwarderProtocolUdp").prop("checked", true); $("#txtAddZoneForwarder").attr("placeholder", "8.8.8.8 or [2620:fe::10]") $("#txtAddZoneForwarder").prop("disabled", true); $("#txtAddZoneForwarder").val("this-server"); $("#divAddZoneForwarderProxy").hide(); } else { $("input[name=rdAddZoneForwarderProtocol]:radio").attr("disabled", false); $("#txtAddZoneForwarder").prop("disabled", false); $("#txtAddZoneForwarder").val(""); $("#divAddZoneForwarderProxy").show(); } } function addZone() { var divAddZoneAlert = $("#divAddZoneAlert"); var zone = $("#txtAddZone").val(); if ((zone == null) || (zone === "")) { showAlert("warning", "Missing!", "Please enter a domain name to add zone.", divAddZoneAlert); $("#txtAddZone").trigger("focus"); return; } var type = $('input[name=rdAddZoneType]:checked').val(); var parameters; switch (type) { case "Primary": var catalog = $("#optAddZoneCatalogZoneName").val(); var useSoaSerialDateScheme = $("#chkAddZoneUseSoaSerialDateScheme").prop("checked"); parameters = "&catalog=" + encodeURIComponent(catalog) + "&useSoaSerialDateScheme=" + useSoaSerialDateScheme; break; case "Secondary": var catalog = $("#optAddZoneCatalogZoneName").val(); parameters = "&catalog=" + encodeURIComponent(catalog) + "&primaryNameServerAddresses=" + encodeURIComponent(cleanTextList($("#txtAddZonePrimaryNameServerAddresses").val())); parameters += "&zoneTransferProtocol=" + $("input[name=rdAddZoneZoneTransferProtocol]:checked").val(); parameters += "&tsigKeyName=" + encodeURIComponent($("#optAddZoneTsigKeyName").val()); parameters += "&validateZone=" + $("#chkAddZoneValidateZone").prop("checked"); break; case "Stub": var catalog = $("#optAddZoneCatalogZoneName").val(); parameters = "&catalog=" + encodeURIComponent(catalog) + "&primaryNameServerAddresses=" + encodeURIComponent(cleanTextList($("#txtAddZonePrimaryNameServerAddresses").val())); break; case "Forwarder": var catalog = $("#optAddZoneCatalogZoneName").val(); var initializeForwarder = $("#chkAddZoneInitializeForwarder").prop("checked"); if (initializeForwarder) { var protocol = $("input[name=rdAddZoneForwarderProtocol]:checked").val(); var forwarder = $("#txtAddZoneForwarder").val(); if ((forwarder == null) || (forwarder === "")) { showAlert("warning", "Missing!", "Please enter a forwarder server address to add zone.", divAddZoneAlert); $("#txtAddZoneForwarder").trigger("focus"); return; } var dnssecValidation = $("#chkAddZoneForwarderDnssecValidation").prop("checked"); parameters = "&catalog=" + encodeURIComponent(catalog) + "&protocol=" + protocol + "&forwarder=" + encodeURIComponent(forwarder) + "&dnssecValidation=" + dnssecValidation; if (forwarder !== "this-server") { var proxyType = $("input[name=rdAddZoneForwarderProxyType]:checked").val(); parameters += "&proxyType=" + proxyType; switch (proxyType) { case "Http": case "Socks5": var proxyAddress = $("#txtAddZoneForwarderProxyAddress").val(); var proxyPort = $("#txtAddZoneForwarderProxyPort").val(); var proxyUsername = $("#txtAddZoneForwarderProxyUsername").val(); var proxyPassword = $("#txtAddZoneForwarderProxyPassword").val(); if ((proxyAddress == null) || (proxyAddress === "")) { showAlert("warning", "Missing!", "Please enter a domain name or IP address for Proxy Server Address to add zone.", divAddZoneAlert); $("#txtAddZoneForwarderProxyAddress").trigger("focus"); return; } if ((proxyPort == null) || (proxyPort === "")) { showAlert("warning", "Missing!", "Please enter a port number for Proxy Server Port to add zone.", divAddZoneAlert); $("#txtAddZoneForwarderProxyPort").trigger("focus"); return; } parameters += "&proxyAddress=" + encodeURIComponent(proxyAddress) + "&proxyPort=" + proxyPort + "&proxyUsername=" + encodeURIComponent(proxyUsername) + "&proxyPassword=" + encodeURIComponent(proxyPassword); break; } } parameters += "&initializeForwarder=true"; } else { parameters = "&initializeForwarder=false"; } break; case "SecondaryForwarder": case "SecondaryCatalog": var primaryNameServerAddresses = cleanTextList($("#txtAddZonePrimaryNameServerAddresses").val()); if ((primaryNameServerAddresses.length === 0) || (primaryNameServerAddresses === ",")) { showAlert("warning", "Missing!", "Please enter at least one primary name server address to proceed.", divAddZoneAlert); $("#txtAddZonePrimaryNameServerAddresses").trigger("focus"); return; } parameters = "&primaryNameServerAddresses=" + encodeURIComponent(primaryNameServerAddresses); parameters += "&zoneTransferProtocol=" + $("input[name=rdAddZoneZoneTransferProtocol]:checked").val(); parameters += "&tsigKeyName=" + encodeURIComponent($("#optAddZoneTsigKeyName").val()); break; case "SecondaryRoot": type = "Secondary"; var catalog = $("#optAddZoneCatalogZoneName").val(); parameters = "&catalog=" + encodeURIComponent(catalog) + "&primaryNameServerAddresses=199.9.14.201,192.33.4.12,199.7.91.13,192.5.5.241,192.112.36.4,193.0.14.129,192.0.47.132,192.0.32.132,[2001:500:200::b],[2001:500:2::c],[2001:500:2d::d],[2001:500:2f::f],[2001:500:12::d0d],[2001:7fd::1],[2620:0:2830:202::132],[2620:0:2d0:202::132]"; parameters += "&zoneTransferProtocol=Tcp"; parameters += "&validateZone=true"; break; default: parameters = ""; break; } var formData; switch (type) { case "Primary": case "Forwarder": var fileAddZoneImportZone = $("#fileAddZoneImportZone"); if (fileAddZoneImportZone[0].files.length > 0) { formData = new FormData(); formData.append("fileImportZone", fileAddZoneImportZone[0].files[0]); } break; } var node = $("#optZonesClusterNode").val(); var btn = $("#btnAddZone"); btn.button("loading"); HTTPRequest({ url: "api/zones/create?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&type=" + type + parameters + "&node=" + encodeURIComponent(node), method: "POST", data: formData, contentType: false, processData: false, success: function (responseJSON) { $("#modalAddZone").modal("hide"); showEditZone(responseJSON.response.domain); showAlert("success", "Zone Added!", "Zone was added successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalAddZone").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divAddZoneAlert }); } function toggleHideDnssecRecords(hideDnssecRecords) { localStorage.setItem("zoneHideDnssecRecords", hideDnssecRecords); showEditZone($("#titleEditZone").attr("data-zone")); } function showEditZone(zone, showPageNumber, zoneFilterName, zoneFilterType) { if (zone == null) { zone = $("#txtZonesEdit").val(); if (zone === "") { showAlert("warning", "Missing!", "Please enter a zone name to start editing."); $("#txtZonesEdit").trigger("focus"); return; } } if (showPageNumber == null) showPageNumber = 1; if (zoneFilterName == null) zoneFilterName = ""; if (zoneFilterType == null) zoneFilterType = ""; var node = $("#optZonesClusterNode").val(); var divViewZonesLoader = $("#divViewZonesLoader"); var divViewZones = $("#divViewZones"); var divEditZone = $("#divEditZone"); divViewZones.hide(); divEditZone.hide(); divViewZonesLoader.show(); HTTPRequest({ url: "api/zones/records/get?token=" + sessionData.token + "&domain=" + encodeURIComponent(zone) + "&zone=" + encodeURIComponent(zone) + "&listZone=true" + "&node=" + encodeURIComponent(node), success: function (responseJSON) { zone = responseJSON.response.zone.name; if (zone === "") zone = "."; var zoneType; if (responseJSON.response.zone.internal) zoneType = "Internal"; else zoneType = responseJSON.response.zone.type; switch (responseJSON.response.zone.dnssecStatus) { case "SignedWithNSEC": case "SignedWithNSEC3": $("#titleEditZoneDnssecStatus").removeClass(); if (responseJSON.response.zone.hasDnssecPrivateKeys) $("#titleEditZoneDnssecStatus").addClass("label label-primary"); else $("#titleEditZoneDnssecStatus").addClass("label label-default"); $("#titleEditZoneDnssecStatus").show(); break; default: $("#titleEditZoneDnssecStatus").hide(); break; } var status; if (responseJSON.response.zone.disabled) status = "Disabled"; else if (responseJSON.response.zone.isExpired) status = "Expired"; else if (responseJSON.response.zone.validationFailed) status = "Validation Failed"; else if (responseJSON.response.zone.syncFailed) status = "Sync Failed"; else if (responseJSON.response.zone.notifyFailed) status = "Notify Failed"; else status = "Enabled"; if (responseJSON.response.zone.catalog != null) { $("#titleEditZoneCatalog").attr("class", "label label-default"); $("#titleEditZoneCatalog").text(responseJSON.response.zone.catalog); $("#titleEditZoneCatalog").show(); } else { switch (zoneType) { case "Catalog": case "SecondaryCatalog": $("#titleEditZoneCatalog").attr("class", "label label-info"); $("#titleEditZoneCatalog").text(zone); $("#titleEditZoneCatalog").show(); break; default: $("#titleEditZoneCatalog").hide(); $("#titleEditZoneCatalog").text(""); break; } } var expiry = responseJSON.response.zone.expiry; if (expiry == null) expiry = " "; else expiry = "Expiry: " + moment(expiry).local().format("YYYY-MM-DD HH:mm:ss"); switch (zoneType) { case "SecondaryForwarder": $("#titleEditZoneType").html("Secondary Forwarder"); break; case "SecondaryCatalog": $("#titleEditZoneType").html("Secondary Catalog"); break; default: $("#titleEditZoneType").html(zoneType); break; } $("#titleEditZoneStatus").html(status); $("#titleEditZoneExpiry").html(expiry); if (responseJSON.response.zone.internal) $("#titleEditZoneType").attr("class", "label label-default"); else $("#titleEditZoneType").attr("class", "label label-primary"); switch (status) { case "Disabled": $("#titleEditZoneStatus").attr("class", "label label-default"); break; case "Sync Failed": case "Notify Failed": $("#titleEditZoneStatus").attr("class", "label label-warning"); break; case "Expired": case "Validation Failed": $("#titleEditZoneStatus").attr("class", "label label-danger"); break; default: $("#titleEditZoneStatus").attr("class", "label label-success"); break; } switch (zoneType) { case "Internal": case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": case "Catalog": $("#btnEditZoneAddRecord").hide(); break; case "Forwarder": $("#btnEditZoneAddRecord").show(); $("#optAddEditRecordTypeDs").hide(); $("#optAddEditRecordTypeSshfp").hide(); $("#optAddEditRecordTypeTlsa").hide(); $("#optAddEditRecordTypeAName").show(); $("#optAddEditRecordTypeFwd").show(); $("#optAddEditRecordTypeApp").show(); break; case "Primary": $("#btnEditZoneAddRecord").show(); $("#optAddEditRecordTypeFwd").hide(); switch (responseJSON.response.zone.dnssecStatus) { case "SignedWithNSEC": case "SignedWithNSEC3": $("#optAddEditRecordTypeDs").show(); $("#optAddEditRecordTypeSshfp").show(); $("#optAddEditRecordTypeTlsa").show(); $("#optAddEditRecordTypeAName").hide(); $("#optAddEditRecordTypeApp").hide(); break; default: $("#optAddEditRecordTypeDs").hide(); $("#optAddEditRecordTypeSshfp").hide(); $("#optAddEditRecordTypeTlsa").hide(); $("#optAddEditRecordTypeAName").show(); $("#optAddEditRecordTypeApp").show(); break; } break; } if (responseJSON.response.zone.internal) { $("#btnEnableZoneEditZone").hide(); $("#btnDisableZoneEditZone").hide(); $("#btnEditZoneDeleteZone").hide(); } else if (responseJSON.response.zone.disabled) { $("#btnEnableZoneEditZone").show(); $("#btnDisableZoneEditZone").hide(); $("#btnEditZoneDeleteZone").show(); } else { $("#btnEnableZoneEditZone").hide(); $("#btnDisableZoneEditZone").show(); $("#btnEditZoneDeleteZone").show(); } switch (zoneType) { case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": $("#btnZoneResync").show(); break; default: $("#btnZoneResync").hide(); break; } switch (zoneType) { case "Primary": case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": case "Forwarder": case "Catalog": $("#divOptionsMenu").show(); break; default: $("#divOptionsMenu").hide(); break; } switch (zoneType) { case "Primary": case "Forwarder": $("#lnkImportZone").show(); $("#lnkExportZone").show(); break; case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Catalog": $("#lnkImportZone").hide(); $("#lnkExportZone").show(); break; default: $("#lnkImportZone").hide(); $("#lnkExportZone").hide(); break; } switch (zoneType) { case "Primary": case "Secondary": case "SecondaryForwarder": case "Forwarder": case "SecondaryCatalog": $("#lnkZoneConvert").show(); break; default: $("#lnkZoneConvert").hide(); break; } switch (zoneType) { case "Primary": case "Forwarder": $("#lnkCloneZone").show(); break; default: $("#lnkCloneZone").hide(); break; } switch (zoneType) { case "Primary": case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": case "Forwarder": case "Catalog": $("#lnkZoneOptions").show(); break; default: $("#lnkZoneOptions").hide(); break; } switch (zoneType) { case "Primary": case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": case "Forwarder": case "Catalog": $("#btnZonePermissions").show(); break; default: $("#btnZonePermissions").hide(); break; } var zoneHideDnssecRecords = (localStorage.getItem("zoneHideDnssecRecords") == "true"); switch (zoneType) { case "Primary": $("#divZoneDnssecOptions").show(); switch (responseJSON.response.zone.dnssecStatus) { case "SignedWithNSEC": case "SignedWithNSEC3": $("#lnkZoneDnssecSignZone").hide(); if (zoneHideDnssecRecords) { $("#lnkZoneDnssecHideRecords").hide(); $("#lnkZoneDnssecShowRecords").show(); } else { $("#lnkZoneDnssecHideRecords").show(); $("#lnkZoneDnssecShowRecords").hide(); } $("#lnkZoneDnssecViewDsRecords").show(); $("#lnkZoneDnssecProperties").show(); $("#lnkZoneDnssecUnsignZone").show(); break; default: $("#lnkZoneDnssecSignZone").show(); $("#lnkZoneDnssecHideRecords").hide(); $("#lnkZoneDnssecShowRecords").hide(); $("#lnkZoneDnssecViewDsRecords").hide(); $("#lnkZoneDnssecProperties").hide(); $("#lnkZoneDnssecUnsignZone").hide(); break; } break; case "Secondary": switch (responseJSON.response.zone.dnssecStatus) { case "SignedWithNSEC": case "SignedWithNSEC3": $("#divZoneDnssecOptions").show(); $("#lnkZoneDnssecSignZone").hide(); if (zoneHideDnssecRecords) { $("#lnkZoneDnssecHideRecords").hide(); $("#lnkZoneDnssecShowRecords").show(); } else { $("#lnkZoneDnssecHideRecords").show(); $("#lnkZoneDnssecShowRecords").hide(); } $("#lnkZoneDnssecViewDsRecords").hide(); $("#lnkZoneDnssecProperties").hide(); $("#lnkZoneDnssecUnsignZone").hide(); break; default: $("#divZoneDnssecOptions").hide(); break; } break; default: $("#divZoneDnssecOptions").hide(); break; } editZoneInfo = responseJSON.response.zone; if (!zoneHideDnssecRecords || (responseJSON.response.zone.dnssecStatus === "Unsigned")) { editZoneRecords = responseJSON.response.records; } else { var records = responseJSON.response.records; editZoneRecords = []; for (var i = 0; i < records.length; i++) { switch (records[i].type.toUpperCase()) { case "RRSIG": case "NSEC": case "DNSKEY": case "NSEC3": case "NSEC3PARAM": continue; default: editZoneRecords.push(records[i]); break; } } } $("#optEditZoneClusterNode").val(node); if (responseJSON.response.zone.nameIdn == null) $("#titleEditZone").text(zone === "." ? "" : zone); else $("#titleEditZone").text(responseJSON.response.zone.nameIdn + " (" + zone + ")"); $("#titleEditZone").attr("data-zone", zone); $("#titleEditZone").attr("data-zone-type", zoneType); $("#txtEditZoneFilterName").val(zoneFilterName); $("#txtEditZoneFilterType").val(zoneFilterType); editZoneFilteredRecords = null; //to evaluate filters again showEditZonePage(showPageNumber); divViewZonesLoader.hide(); divEditZone.show(); }, error: function () { divViewZonesLoader.hide(); divViewZones.show(); }, invalidToken: function () { showPageLogin(); }, objLoaderPlaceholder: divViewZonesLoader }); } function showEditZonePage(pageNumber) { var filterName = $("#txtEditZoneFilterName").val(); if (filterName === "") filterName = null; var filterType = $("#txtEditZoneFilterType").val(); if (filterType === "") filterType = null; if (pageNumber == null) pageNumber = Number($("#txtEditZonePageNumber").val()); if (pageNumber == 0) pageNumber = 1; var recordsPerPage = Number($("#optEditZoneRecordsPerPage").val()); if (recordsPerPage < 1) recordsPerPage = 10; var zone = $("#titleEditZone").attr("data-zone"); var zoneType = $("#titleEditZone").attr("data-zone-type"); if (editZoneFilteredRecords == null) { if ((filterName != null) || (filterType != null)) { editZoneFilteredRecords = []; var filterDomain = null; var filterRegex = null; if (filterName != null) { filterDomain = filterName.toLowerCase(); if (zone == ".") { if (filterDomain === "@") filterDomain = ""; } else { if (filterDomain === "@") filterDomain = zone; else filterDomain += "." + zone; } if ((filterName.indexOf("*") > -1) || (filterName.indexOf("?") > -1)) { filterDomain = filterDomain.replace(/\./g, "\\\."); filterDomain = filterDomain.replace(/\*/g, ".*"); filterDomain = filterDomain.replace(/\?/g, "."); if (filterDomain.startsWith(".*\\\.")) filterDomain = "\\\*" + filterDomain.substring(2); filterRegex = new RegExp("^" + filterDomain + "$"); } } if (filterType != null) filterType = filterType.toUpperCase(); for (var i = 0; i < editZoneRecords.length; i++) { if (filterRegex == null) { if ((filterDomain != null) && (editZoneRecords[i].name.toLowerCase() !== filterDomain)) continue; } else if (!filterRegex.test(editZoneRecords[i].name.toLowerCase())) { continue; } if ((filterType != null) && (editZoneRecords[i].type !== filterType)) continue; editZoneRecords[i].index = i; //keep original index for update tasks editZoneFilteredRecords.push(editZoneRecords[i]); } } else { for (var i = 0; i < editZoneRecords.length; i++) editZoneRecords[i].index = i; //keep original index for update tasks editZoneFilteredRecords = editZoneRecords; } } var totalRecords = editZoneFilteredRecords.length; var totalPages = Math.floor(totalRecords / recordsPerPage) + (totalRecords % recordsPerPage > 0 ? 1 : 0); if ((pageNumber > totalPages) || (pageNumber < 0)) pageNumber = totalPages; if (pageNumber < 1) pageNumber = 1; var start = (pageNumber - 1) * recordsPerPage; var end = Math.min(start + recordsPerPage, totalRecords); var tableHtmlRows = ""; for (var i = start; i < end; i++) tableHtmlRows += getZoneRecordRowHtml(i, zone, zoneType, editZoneFilteredRecords[i]); var paginationHtml = ""; if (pageNumber > 1) { paginationHtml += "
  • «
  • "; paginationHtml += "
  • "; } var pageStart = pageNumber - 5; if (pageStart < 1) pageStart = 1; var pageEnd = pageStart + 9; if (pageEnd > totalPages) { var endDiff = pageEnd - totalPages; pageEnd = totalPages; pageStart -= endDiff; if (pageStart < 1) pageStart = 1; } for (var i = pageStart; i <= pageEnd; i++) { if (i == pageNumber) paginationHtml += "
  • " + i + "
  • "; else paginationHtml += "
  • " + i + "
  • "; } if (pageNumber < totalPages) { paginationHtml += "
  • "; paginationHtml += "
  • »
  • "; } var statusHtml; if (editZoneFilteredRecords.length > 0) statusHtml = (start + 1) + "-" + end + " (" + (end - start) + ") of " + editZoneFilteredRecords.length + " records (page " + pageNumber + " of " + totalPages + ")"; else statusHtml = "0 records"; $("#txtEditZonePageNumber").val(pageNumber); $("#tableEditZoneBody").html(tableHtmlRows); $("#tableEditZoneTopStatus").html(statusHtml); $("#tableEditZoneTopPagination").html(paginationHtml); $("#tableEditZoneFooterStatus").html(statusHtml); $("#tableEditZoneFooterPagination").html(paginationHtml); } function getZoneRecordRowHtml(index, zone, zoneType, record) { var name = record.name; if (name === "") name = "."; var lowerName = name.toLowerCase(); if (lowerName === zone) { name = "@"; } else { var i = lowerName.lastIndexOf("." + zone) if (i > -1) name = name.substring(0, i); } var tableHtmlRow = "" + (index + 1) + "" + htmlEncode(name) + ""; tableHtmlRow += "" + record.type + ""; tableHtmlRow += "" + record.ttl + "
    (" + record.ttlString + ")"; var additionalDataAttributes = ""; tableHtmlRow += ""; switch (record.type.toUpperCase()) { case "A": case "AAAA": tableHtmlRow += htmlEncode(record.rData.ipAddress); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-ip-address=\"" + htmlEncode(record.rData.ipAddress) + "\" "; break; case "NS": var notifyFailed = false; if (editZoneInfo.notifyFailedFor != null) { for (var i = 0; i < editZoneInfo.notifyFailedFor.length; i++) { if (editZoneInfo.notifyFailedFor[i] == record.rData.nameServer) { notifyFailed = true; break; } } } tableHtmlRow += "Name Server: " + htmlEncode(record.rData.nameServer); if (notifyFailed) tableHtmlRow += "Notify Failed"; if (record.glueRecords != null) { var glue = null; for (var i = 0; i < record.glueRecords.length; i++) { if (i == 0) glue = record.glueRecords[i]; else glue += ", " + record.glueRecords[i]; } tableHtmlRow += "
    Glue Addresses: " + glue; additionalDataAttributes = "data-record-glue=\"" + htmlEncode(glue) + "\" "; } else { additionalDataAttributes = "data-record-glue=\"\" "; } tableHtmlRow += "

    "; additionalDataAttributes += "data-record-name-server=\"" + htmlEncode(record.rData.nameServer) + "\" "; break; case "CNAME": tableHtmlRow += htmlEncode(record.rData.cname); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-cname=\"" + htmlEncode(record.rData.cname) + "\" "; break; case "SOA": tableHtmlRow += "Primary Name Server: " + htmlEncode(record.rData.primaryNameServer) + "
    Responsible Person: " + htmlEncode(record.rData.responsiblePerson) + "
    Serial: " + htmlEncode(record.rData.serial) + "
    Refresh: " + htmlEncode(record.rData.refresh + " (" + record.rData.refreshString + ")") + "
    Retry: " + htmlEncode(record.rData.retry + " (" + record.rData.retryString + ")") + "
    Expire: " + htmlEncode(record.rData.expire + " (" + record.rData.expireString + ")") + "
    Minimum: " + htmlEncode(record.rData.minimum + " (" + record.rData.minimumString + ")"); if (record.rData.useSerialDateScheme != null) { tableHtmlRow += "

    Use Serial Date Scheme: " + record.rData.useSerialDateScheme; additionalDataAttributes = "data-record-serial-scheme=\"" + htmlEncode(record.rData.useSerialDateScheme) + "\" "; } else { additionalDataAttributes = "data-record-serial-scheme=\"false\" "; } tableHtmlRow += "

    "; additionalDataAttributes += "data-record-pname=\"" + htmlEncode(record.rData.primaryNameServer) + "\" " + "data-record-rperson=\"" + htmlEncode(record.rData.responsiblePerson) + "\" " + "data-record-serial=\"" + htmlEncode(record.rData.serial) + "\" " + "data-record-refresh=\"" + htmlEncode(record.rData.refresh) + "\" " + "data-record-retry=\"" + htmlEncode(record.rData.retry) + "\" " + "data-record-expire=\"" + htmlEncode(record.rData.expire) + "\" " + "data-record-minimum=\"" + htmlEncode(record.rData.minimum) + "\" "; break; case "PTR": tableHtmlRow += htmlEncode(record.rData.ptrName); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-ptr-name=\"" + htmlEncode(record.rData.ptrName) + "\" "; break; case "MX": tableHtmlRow += "Preference: " + htmlEncode(record.rData.preference) + "
    Exchange: " + htmlEncode(record.rData.exchange); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-preference=\"" + htmlEncode(record.rData.preference) + "\" " + "data-record-exchange=\"" + htmlEncode(record.rData.exchange) + "\" "; break; case "TXT": var text; if (record.rData.splitText) { for (var i = 0; i < record.rData.characterStrings.length; i++) { var characterString = record.rData.characterStrings[i].replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n"); tableHtmlRow += "\"" + htmlEncode(characterString.replace(/"/g, "\\\"")) + "\"
    "; if (text == null) text = characterString; else text += "\n" + characterString; } } else { var characterString = record.rData.text.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n"); tableHtmlRow += htmlEncode(characterString.replace(/"/g, "\\\"")) + "
    "; text = record.rData.text; } tableHtmlRow += "
    "; additionalDataAttributes = "data-record-text=\"" + htmlEncode(text) + "\" " + "data-record-split-text=\"" + htmlEncode(record.rData.splitText) + "\" "; break; case "RP": tableHtmlRow += "Mailbox: " + htmlEncode(record.rData.mailbox) + "
    TXT Domain: " + htmlEncode(record.rData.txtDomain); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-mailbox=\"" + htmlEncode(record.rData.mailbox) + "\" " + "data-record-txt-domain=\"" + htmlEncode(record.rData.txtDomain) + "\" "; break; case "SRV": tableHtmlRow += "Priority: " + htmlEncode(record.rData.priority) + "
    Weight: " + htmlEncode(record.rData.weight) + "
    Port: " + htmlEncode(record.rData.port) + "
    Target: " + htmlEncode(record.rData.target); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-priority=\"" + htmlEncode(record.rData.priority) + "\" " + "data-record-weight=\"" + htmlEncode(record.rData.weight) + "\" " + "data-record-port=\"" + htmlEncode(record.rData.port) + "\" " + "data-record-target=\"" + htmlEncode(record.rData.target) + "\" "; break; case "NAPTR": tableHtmlRow += "Order: " + htmlEncode(record.rData.order) + "
    Preference: " + htmlEncode(record.rData.preference) + "
    Flags: " + htmlEncode(record.rData.flags) + "
    Services: " + htmlEncode(record.rData.services) + "
    Regular Expression: " + htmlEncode(record.rData.regexp) + "
    Replacement: " + htmlEncode(record.rData.replacement); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-order=\"" + htmlEncode(record.rData.order) + "\" " + "data-record-preference=\"" + htmlEncode(record.rData.preference) + "\" " + "data-record-flags=\"" + htmlEncode(record.rData.flags) + "\" " + "data-record-services=\"" + htmlEncode(record.rData.services) + "\" " + "data-record-regexp=\"" + htmlEncode(record.rData.regexp) + "\" " + "data-record-replacement=\"" + htmlEncode(record.rData.replacement) + "\" "; break; case "DNAME": tableHtmlRow += htmlEncode(record.rData.dname); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-dname=\"" + htmlEncode(record.rData.dname) + "\" "; break; case "APL": tableHtmlRow += ""; for (var i = 0; i < record.rData.addressPrefixes.length; i++) { tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; } tableHtmlRow += "
    FamilyNegationAFD PartPrefix
    " + record.rData.addressPrefixes[i].addressFamily + "" + record.rData.addressPrefixes[i].negation + "" + record.rData.addressPrefixes[i].afdPart + "" + record.rData.addressPrefixes[i].prefix + "
    "; additionalDataAttributes = ""; break; case "DS": tableHtmlRow += "Key Tag: " + htmlEncode(record.rData.keyTag) + "
    Algorithm: " + htmlEncode(record.rData.algorithm + " (" + record.rData.algorithmNumber + ")") + "
    Digest Type: " + htmlEncode(record.rData.digestType + " (" + record.rData.digestTypeNumber + ")") + "
    Digest: " + htmlEncode(record.rData.digest); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-key-tag=\"" + htmlEncode(record.rData.keyTag) + "\" " + "data-record-algorithm=\"" + htmlEncode(record.rData.algorithm) + "\" " + "data-record-digest-type=\"" + htmlEncode(record.rData.digestType) + "\" " + "data-record-digest=\"" + htmlEncode(record.rData.digest) + "\" "; break; case "SSHFP": tableHtmlRow += "Algorithm: " + htmlEncode(record.rData.algorithm) + "
    Fingerprint Type: " + htmlEncode(record.rData.fingerprintType) + "
    Fingerprint: " + htmlEncode(record.rData.fingerprint); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-algorithm=\"" + htmlEncode(record.rData.algorithm) + "\" " + "data-record-fingerprint-type=\"" + htmlEncode(record.rData.fingerprintType) + "\" " + "data-record-fingerprint=\"" + htmlEncode(record.rData.fingerprint) + "\" "; break; case "RRSIG": tableHtmlRow += "Type Covered: " + htmlEncode(record.rData.typeCovered) + "
    Algorithm: " + htmlEncode(record.rData.algorithm + " (" + record.rData.algorithmNumber + ")") + "
    Labels: " + htmlEncode(record.rData.labels) + "
    Original TTL: " + htmlEncode(record.rData.originalTtl) + "
    Signature Expiration: " + moment(record.rData.signatureExpiration).local().format("YYYY-MM-DD HH:mm:ss") + "
    Signature Inception: " + moment(record.rData.signatureInception).local().format("YYYY-MM-DD HH:mm:ss") + "
    Key Tag: " + htmlEncode(record.rData.keyTag) + "
    Signer's Name: " + htmlEncode(record.rData.signersName) + "
    Signature: " + htmlEncode(record.rData.signature); tableHtmlRow += "

    "; additionalDataAttributes = ""; break; case "NSEC": var nsecTypes = null; for (var j = 0; j < record.rData.types.length; j++) { if (nsecTypes == null) nsecTypes = record.rData.types[j]; else nsecTypes += ", " + record.rData.types[j]; } tableHtmlRow += "Next Domain Name: " + htmlEncode(record.rData.nextDomainName) + "
    Types: " + htmlEncode(nsecTypes); tableHtmlRow += "

    "; additionalDataAttributes = ""; break; case "DNSKEY": tableHtmlRow += "Flags: " + htmlEncode(record.rData.flags) + "
    Protocol: " + htmlEncode(record.rData.protocol) + "
    Algorithm: " + htmlEncode(record.rData.algorithm + " (" + record.rData.algorithmNumber + ")") + "
    Public Key: " + htmlEncode(record.rData.publicKey); if (record.rData.dnsKeyState == null) { tableHtmlRow += "
    "; } else { if (record.rData.dnsKeyStateReadyBy != null) tableHtmlRow += "

    Key State: " + htmlEncode(record.rData.dnsKeyState) + " (ready by: " + moment(record.rData.dnsKeyStateReadyBy).local().format("YYYY-MM-DD HH:mm") + ")"; else if (record.rData.dnsKeyStateActiveBy != null) tableHtmlRow += "

    Key State: " + htmlEncode(record.rData.dnsKeyState) + " (active by: " + moment(record.rData.dnsKeyStateActiveBy).local().format("YYYY-MM-DD HH:mm") + ")"; else tableHtmlRow += "

    Key State: " + htmlEncode(record.rData.dnsKeyState); } tableHtmlRow += "
    Computed Key Tag: " + htmlEncode(record.rData.computedKeyTag); if (record.rData.computedDigests != null) { tableHtmlRow += "
    Computed Digests: "; for (var j = 0; j < record.rData.computedDigests.length; j++) { tableHtmlRow += "
    " + htmlEncode(record.rData.computedDigests[j].digestType) + ": " + htmlEncode(record.rData.computedDigests[j].digest) } } tableHtmlRow += "

    "; additionalDataAttributes = ""; break; case "NSEC3": var nsec3Types = null; for (var j = 0; j < record.rData.types.length; j++) { if (nsec3Types == null) nsec3Types = record.rData.types[j]; else nsec3Types += ", " + record.rData.types[j]; } tableHtmlRow += "Hash Algorithm: " + htmlEncode(record.rData.hashAlgorithm) + "
    Flags: " + htmlEncode(record.rData.flags) + "
    Iterations: " + htmlEncode(record.rData.iterations) + "
    Salt: " + htmlEncode(record.rData.salt) + "
    Next Hashed Owner Name: " + htmlEncode(record.rData.nextHashedOwnerName) + "
    Types: " + htmlEncode(nsec3Types); tableHtmlRow += "

    "; additionalDataAttributes = ""; break; case "NSEC3PARAM": tableHtmlRow += "Hash Algorithm: " + htmlEncode(record.rData.hashAlgorithm) + "
    Flags: " + htmlEncode(record.rData.flags) + "
    Iterations: " + htmlEncode(record.rData.iterations) + "
    Salt: " + htmlEncode(record.rData.salt); tableHtmlRow += "

    "; additionalDataAttributes = ""; break; case "TLSA": tableHtmlRow += "Certificate Usage: " + htmlEncode(record.rData.certificateUsage) + "
    Selector: " + htmlEncode(record.rData.selector) + "
    Matching Type: " + htmlEncode(record.rData.matchingType) + "
    Certificate Association Data: " + (record.rData.certificateAssociationData == "" ? "
    " : "
    " + htmlEncode(record.rData.certificateAssociationData) + "
    "); tableHtmlRow += "
    "; additionalDataAttributes = "data-record-certificate-usage=\"" + htmlEncode(record.rData.certificateUsage) + "\" " + "data-record-selector=\"" + htmlEncode(record.rData.selector) + "\" " + "data-record-matching-type=\"" + htmlEncode(record.rData.matchingType) + "\" " + "data-record-certificate-association-data=\"" + htmlEncode(record.rData.certificateAssociationData) + "\" "; break; case "ZONEMD": tableHtmlRow += "Serial: " + htmlEncode(record.rData.serial) + "
    Scheme: " + htmlEncode(record.rData.scheme) + "
    Hash Algorithm: " + htmlEncode(record.rData.hashAlgorithm) + "
    Digest: " + record.rData.digest; tableHtmlRow += "

    "; additionalDataAttributes = ""; break; case "SVCB": case "HTTPS": var tableHtmlSvcParams; if (Object.keys(record.rData.svcParams).length == 0) { tableHtmlSvcParams = "
    "; } else { tableHtmlSvcParams = "
    Params: " + "" + "" + "" + "" + ""; for (var paramKey in record.rData.svcParams) { switch (paramKey) { case "ipv4hint": if (record.rData.autoIpv4Hint) continue; break; case "ipv6hint": if (record.rData.autoIpv6Hint) continue; break; } tableHtmlSvcParams += ""; } tableHtmlSvcParams += "
    KeyValue
    " + htmlEncode(paramKey) + "" + htmlEncode(record.rData.svcParams[paramKey]) + "
    "; } tableHtmlRow += "Priority: " + htmlEncode(record.rData.svcPriority) + (record.rData.svcPriority == 0 ? " (alias mode)" : " (service mode)") + "
    Target Name: " + (record.rData.svcTargetName == "" ? "." : htmlEncode(record.rData.svcTargetName)) + tableHtmlSvcParams + "
    Use Automatic IPv4 Hint: " + record.rData.autoIpv4Hint + "
    Use Automatic IPv6 Hint: " + record.rData.autoIpv6Hint + "
    "; tableHtmlRow += "
    "; additionalDataAttributes = "data-record-svc-priority=\"" + htmlEncode(record.rData.svcPriority) + "\"" + "data-record-svc-target-name=\"" + (record.rData.svcTargetName == "" ? "." : htmlEncode(record.rData.svcTargetName)) + "\"" + "data-record-svc-params=\"" + htmlEncode(JSON.stringify(record.rData.svcParams)) + "\"" + "data-record-auto-ipv4hint=\"" + htmlEncode(record.rData.autoIpv4Hint) + "\"" + "data-record-auto-ipv6hint=\"" + htmlEncode(record.rData.autoIpv6Hint) + "\""; break; case "URI": tableHtmlRow += "Priority: " + htmlEncode(record.rData.priority) + "
    Weight: " + htmlEncode(record.rData.weight) + "
    URI: " + htmlEncode(record.rData.uri); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-priority=\"" + htmlEncode(record.rData.priority) + "\" " + "data-record-weight=\"" + htmlEncode(record.rData.weight) + "\" " + "data-record-uri=\"" + htmlEncode(record.rData.uri) + "\" "; break; case "CAA": tableHtmlRow += "Flags: " + htmlEncode(record.rData.flags) + "
    Tag: " + htmlEncode(record.rData.tag) + "
    Authority: " + htmlEncode(record.rData.value); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-flags=\"" + htmlEncode(record.rData.flags) + "\" " + "data-record-tag=\"" + htmlEncode(record.rData.tag) + "\" " + "data-record-value=\"" + htmlEncode(record.rData.value) + "\" "; break; case "ANAME": tableHtmlRow += "" + htmlEncode(record.rData.aname); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-aname=\"" + htmlEncode(record.rData.aname) + "\" "; break; case "FWD": tableHtmlRow += "Protocol: " + htmlEncode(record.rData.protocol) + "
    Forwarder: " + htmlEncode(record.rData.forwarder) + "
    Priority: " + htmlEncode(record.rData.priority) + "
    Enable DNSSEC Validation: " + htmlEncode(record.rData.dnssecValidation) + "
    Proxy Type: " + htmlEncode(record.rData.proxyType); switch (record.rData.proxyType) { case "Http": case "Socks5": tableHtmlRow += "
    Proxy Address: " + htmlEncode(record.rData.proxyAddress) + "
    Proxy Port: " + htmlEncode(record.rData.proxyPort) + "
    Proxy Username: " + htmlEncode(record.rData.proxyUsername) + "
    Proxy Password: ************"; break; } tableHtmlRow += "

    "; additionalDataAttributes = "data-record-protocol=\"" + htmlEncode(record.rData.protocol) + "\" " + "data-record-forwarder=\"" + htmlEncode(record.rData.forwarder) + "\" " + "data-record-priority=\"" + htmlEncode(record.rData.priority) + "\" " + "data-record-dnssec-validation=\"" + htmlEncode(record.rData.dnssecValidation) + "\" " + "data-record-proxy-type=\"" + htmlEncode(record.rData.proxyType) + "\" "; switch (record.rData.proxyType) { case "Http": case "Socks5": additionalDataAttributes += "data-record-proxy-address=\"" + htmlEncode(record.rData.proxyAddress) + "\" " + "data-record-proxy-port=\"" + htmlEncode(record.rData.proxyPort) + "\" " + "data-record-proxy-username=\"" + htmlEncode(record.rData.proxyUsername) + "\" " + "data-record-proxy-password=\"" + htmlEncode(record.rData.proxyPassword) + "\" "; break; } break; case "APP": tableHtmlRow += "App Name: " + htmlEncode(record.rData.appName) + "
    Class Path: " + htmlEncode(record.rData.classPath) + "
    Record Data: " + (record.rData.data == "" ? "
    " : "
    " + htmlEncode(record.rData.data) + "
    "); tableHtmlRow += "
    "; additionalDataAttributes = "data-record-app-name=\"" + htmlEncode(record.rData.appName) + "\" " + "data-record-classpath=\"" + htmlEncode(record.rData.classPath) + "\" " + "data-record-data=\"" + htmlEncode(record.rData.data) + "\""; break; case "ALIAS": tableHtmlRow += "Type: " + htmlEncode(record.rData.type) + "
    Alias: " + htmlEncode(record.rData.alias); tableHtmlRow += "

    "; break; default: tableHtmlRow += "RDATA: " + htmlEncode(record.rData.value); tableHtmlRow += "

    "; additionalDataAttributes = "data-record-rdata=\"" + htmlEncode(record.rData.value) + "\""; break; } if (record.expiryTtl > 0) { var expiresOn = moment(record.lastModified).add(record.expiryTtl, "s"); tableHtmlRow += "Expiry TTL: " + record.expiryTtl + " (" + record.expiryTtlString + ")"; tableHtmlRow += "
    Expires On: " + expiresOn.local().format("YYYY-MM-DD HH:mm:ss") + " (" + expiresOn.fromNow() + ")"; tableHtmlRow += "
    "; } var lastUsedOn; if (record.lastUsedOn == "0001-01-01T00:00:00") lastUsedOn = moment(record.lastUsedOn).local().format("YYYY-MM-DD HH:mm:ss") + " (never)"; else lastUsedOn = moment(record.lastUsedOn).local().format("YYYY-MM-DD HH:mm:ss") + " (" + moment(record.lastUsedOn).fromNow() + ")"; tableHtmlRow += "Last Used: " + lastUsedOn; if ((record.lastModified != "0001-01-01T00:00:00") && (record.lastModified != "0001-01-01T00:00:00Z")) tableHtmlRow += "
    Last Modified: " + moment(record.lastModified).local().format("YYYY-MM-DD HH:mm:ss") + " (" + moment(record.lastModified).fromNow() + ")";; if ((record.comments != null) && (record.comments.length > 0)) tableHtmlRow += "
    Comments:
    " + htmlEncode(record.comments) + "
    "; tableHtmlRow += ""; var hideActionButtons = false; var disableEnableDisableDeleteButtons = false; switch (zoneType) { case "Internal": case "Secondary": case "SecondaryForwarder": case "SecondaryCatalog": case "Stub": hideActionButtons = true; break; case "Catalog": switch (record.type) { case "SOA": disableEnableDisableDeleteButtons = true; break; default: hideActionButtons = true; break; } break; default: switch (record.type) { case "SOA": disableEnableDisableDeleteButtons = true; break; case "DNSKEY": case "RRSIG": case "NSEC": case "NSEC3": case "NSEC3PARAM": case "ZONEMD": hideActionButtons = true; break; } break; } if (hideActionButtons) { tableHtmlRow += " "; } else { tableHtmlRow += ""; tableHtmlRow += "
    "; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; tableHtmlRow += ""; } tableHtmlRow += ""; return tableHtmlRow; } function clearAddEditRecordForm() { $("#divAddEditRecordAlert").html(""); $("#txtAddEditRecordName").prop("placeholder", "@"); $("#txtAddEditRecordName").prop("disabled", false); $("#optAddEditRecordType").prop("disabled", false); $("#txtAddEditRecordTtl").prop("disabled", false); $("#txtAddEditRecordName").val(""); $("#optAddEditRecordType").val("A"); $("#txtAddEditRecordTtl").val(""); $("#txtAddEditRecordTtl").attr("placeholder", sessionData.info.defaultRecordTtl); $("#spanAddEditRecordTtlUnit").text("seconds (default " + sessionData.info.defaultRecordTtl + ")"); $("#divAddEditRecordData").show(); $("#divAddEditRecordDataUnknownType").hide(); $("#txtAddEditRecordDataUnknownType").val(""); $("#txtAddEditRecordDataUnknownType").prop("disabled", false); $("#lblAddEditRecordDataValue").text("IPv4 Address"); $("#txtAddEditRecordDataValue").val(""); $("#divAddEditRecordDataPtr").show(); $("#chkAddEditRecordDataPtr").prop("checked", false); $("#chkAddEditRecordDataCreatePtrZone").prop("disabled", true); $("#chkAddEditRecordDataCreatePtrZone").prop("checked", false); $("#chkAddEditRecordDataPtrLabel").text("Add reverse (PTR) record"); $("#divAddEditRecordDataNs").hide(); $("#txtAddEditRecordDataNsNameServer").prop("disabled", false); $("#txtAddEditRecordDataNsNameServer").val(""); $("#txtAddEditRecordDataNsGlue").prop("disabled", false); $("#txtAddEditRecordDataNsGlue").val(""); $("#divEditRecordDataSoa").hide(); $("#txtEditRecordDataSoaPrimaryNameServer").prop("disabled", false); $("#txtEditRecordDataSoaResponsiblePerson").prop("disabled", false); $("#txtEditRecordDataSoaSerial").prop("disabled", false); $("#txtEditRecordDataSoaRefresh").prop("disabled", false); $("#txtEditRecordDataSoaRetry").prop("disabled", false); $("#txtEditRecordDataSoaExpire").prop("disabled", false); $("#txtEditRecordDataSoaMinimum").prop("disabled", false); $("#txtEditRecordDataSoaPrimaryNameServer").val(""); $("#txtEditRecordDataSoaResponsiblePerson").val(""); $("#txtEditRecordDataSoaSerial").val(""); $("#txtEditRecordDataSoaRefresh").val(""); $("#txtEditRecordDataSoaRetry").val(""); $("#txtEditRecordDataSoaExpire").val(""); $("#txtEditRecordDataSoaMinimum").val(""); $("#divAddEditRecordDataMx").hide(); $("#txtAddEditRecordDataMxPreference").val(""); $("#txtAddEditRecordDataMxExchange").val(""); $("#divAddEditRecordDataTxt").hide(); $("#txtAddEditRecordDataTxt").val(""); $("#chkAddEditRecordDataTxtSplitText").prop("checked", false); $("#divAddEditRecordDataSrv").hide(); $("#txtAddEditRecordDataSrvPriority").val(""); $("#txtAddEditRecordDataSrvWeight").val(""); $("#txtAddEditRecordDataSrvPort").val(""); $("#txtAddEditRecordDataSrvTarget").val(""); $("#divAddEditRecordDataNaptr").hide(); $("#txtAddEditRecordDataNaptrOrder").val(""); $("#txtAddEditRecordDataNaptrPreference").val(""); $("#txtAddEditRecordDataNaptrFlags").val(""); $("#txtAddEditRecordDataNaptrServices").val(""); $("#txtAddEditRecordDataNaptrRegExp").val(""); $("#txtAddEditRecordDataNaptrReplacement").val(""); $("#divAddEditRecordDataDs").hide(); $("#txtAddEditRecordDataDsKeyTag").val(""); $("#optAddEditRecordDataDsAlgorithm").val(""); $("#optAddEditRecordDataDsDigestType").val(""); $("#txtAddEditRecordDataDsDigest").val(""); $("#divAddEditRecordDataSshfp").hide(); $("#optAddEditRecordDataSshfpAlgorithm").val(""); $("#optAddEditRecordDataSshfpFingerprintType").val(""); $("#txtAddEditRecordDataSshfpFingerprint").val(""); $("#divAddEditRecordDataTlsa").hide(); $("#optAddEditRecordDataTlsaCertificateUsage").val(""); $("#optAddEditRecordDataTlsaSelector").val(""); $("#optAddEditRecordDataTlsaMatchingType").val(""); $("#txtAddEditRecordDataTlsaCertificateAssociationData").val(""); $("#divAddEditRecordDataSvcb").hide(); $("#txtAddEditRecordDataSvcbPriority").val(""); $("#txtAddEditRecordDataSvcbTargetName").val(""); $("#tableAddEditRecordDataSvcbParams").html(""); $("#chkAddEditRecordDataSvcbAutoIpv4Hint").prop("checked", false); $("#chkAddEditRecordDataSvcbAutoIpv6Hint").prop("checked", false); $("#divAddEditRecordDataUri").hide(); $("#txtAddEditRecordDataUriPriority").val(""); $("#txtAddEditRecordDataUriWeight").val(""); $("#txtAddEditRecordDataUri").val(""); $("#divAddEditRecordDataCaa").hide(); $("#txtAddEditRecordDataCaaFlags").val(""); $("#txtAddEditRecordDataCaaTag").val(""); $("#txtAddEditRecordDataCaaValue").val(""); $("#divAddEditRecordDataForwarder").hide(); $("#rdAddEditRecordDataForwarderProtocolUdp").prop("checked", true); $("input[name=rdAddEditRecordDataForwarderProtocol]:radio").attr("disabled", false); $("#chkAddEditRecordDataForwarderThisServer").prop("checked", false); $('#txtAddEditRecordDataForwarder').prop("disabled", false); $("#txtAddEditRecordDataForwarder").attr("placeholder", "8.8.8.8 or [2620:fe::10]") $("#txtAddEditRecordDataForwarder").val(""); $("#txtAddEditRecordDataForwarderPriority").val(""); $("#chkAddEditRecordDataForwarderDnssecValidation").prop("checked", $("#chkDnssecValidation").prop("checked")); $("#rdAddEditRecordDataForwarderProxyTypeDefaultProxy").prop("checked", true); $("#txtAddEditRecordDataForwarderProxyAddress").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyPort").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyUsername").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyPassword").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyAddress").val(""); $("#txtAddEditRecordDataForwarderProxyPort").val(""); $("#txtAddEditRecordDataForwarderProxyUsername").val(""); $("#txtAddEditRecordDataForwarderProxyPassword").val(""); $("#divAddEditRecordDataApplication").hide(); $("#optAddEditRecordDataAppName").html(""); $("#optAddEditRecordDataAppName").prop("disabled", false); $("#optAddEditRecordDataClassPath").html(""); $("#optAddEditRecordDataClassPath").prop("disabled", false); $("#txtAddEditRecordDataData").val(""); $("#divAddEditRecordOverwrite").show(); $("#chkAddEditRecordOverwrite").prop("checked", false); $("#txtAddEditRecordComments").val(""); $("#divAddEditRecordExpiryTtl").show(); $("#txtAddEditRecordExpiryTtl").prop("disabled", false); $("#txtAddEditRecordExpiryTtl").val(""); $("#btnAddEditRecord").button("reset"); } function showAddRecordModal() { var zone = $("#titleEditZone").attr("data-zone"); var lastType = $("#optAddEditRecordType").val(); clearAddEditRecordForm(); if (zone.endsWith(".in-addr.arpa") || zone.endsWith(".ip6.arpa")) { $("#optAddEditRecordType").val("PTR"); modifyAddRecordFormByType(true); } else if (lastType != "SOA") { $("#optAddEditRecordType").val(lastType); modifyAddRecordFormByType(true); } $("#titleAddEditRecord").text("Add Record"); $("#lblAddEditRecordZoneName").text(zone === "." ? "" : zone); $("#optEditRecordTypeSoa").hide(); $("#btnAddEditRecord").attr("onclick", "addRecord(); return false;"); $("#modalAddEditRecord").modal("show"); setTimeout(function () { $("#txtAddEditRecordName").trigger("focus"); }, 1000); } var appsList; function loadAddRecordModalAppNames() { var optAddEditRecordDataAppName = $("#optAddEditRecordDataAppName"); var optAddEditRecordDataClassPath = $("#optAddEditRecordDataClassPath"); var txtAddEditRecordDataData = $("#txtAddEditRecordDataData"); var divAddEditRecordAlert = $("#divAddEditRecordAlert"); optAddEditRecordDataAppName.prop("disabled", true); optAddEditRecordDataClassPath.prop("disabled", true); txtAddEditRecordDataData.prop("disabled", true); optAddEditRecordDataAppName.html(""); optAddEditRecordDataClassPath.html(""); txtAddEditRecordDataData.val(""); var node = $("#optZonesClusterNode").val(); HTTPRequest({ url: "api/apps/list?token=" + sessionData.token + "&node=" + encodeURIComponent(node), success: function (responseJSON) { appsList = responseJSON.response.apps; var optApps = ""; var optClassPaths = ""; for (var i = 0; i < appsList.length; i++) { for (var j = 0; j < appsList[i].dnsApps.length; j++) { if (appsList[i].dnsApps[j].isAppRecordRequestHandler) { optApps += ""; break; } } } $("#optAddEditRecordDataAppName").html(optApps); $("#optAddEditRecordDataClassPath").html(optClassPaths); optAddEditRecordDataAppName.prop("disabled", false); optAddEditRecordDataClassPath.prop("disabled", false); txtAddEditRecordDataData.prop("disabled", false); }, invalidToken: function () { showPageLogin(); }, objAlertPlaceholder: divAddEditRecordAlert }); } function modifyAddRecordFormByType(addMode) { $("#divAddEditRecordAlert").html(""); $("#txtAddEditRecordName").prop("placeholder", "@"); $("#txtAddEditRecordTtl").prop("disabled", false); $("#txtAddEditRecordTtl").val(""); $("#txtAddEditRecordTtl").attr("placeholder", sessionData.info.defaultRecordTtl); $("#spanAddEditRecordTtlUnit").text("seconds (default " + sessionData.info.defaultRecordTtl + ")"); $("#txtAddEditRecordDataValue").attr("placeholder", ""); var type = $("#optAddEditRecordType").val(); $("#divAddEditRecordData").hide(); $("#divAddEditRecordDataUnknownType").hide(); $("#divAddEditRecordDataPtr").hide(); $("#divAddEditRecordDataNs").hide(); $("#divEditRecordDataSoa").hide(); $("#divAddEditRecordDataMx").hide(); $("#divAddEditRecordDataTxt").hide(); $("#divAddEditRecordDataRp").hide(); $("#divAddEditRecordDataSrv").hide(); $("#divAddEditRecordDataNaptr").hide(); $("#divAddEditRecordDataDs").hide(); $("#divAddEditRecordDataSshfp").hide(); $("#divAddEditRecordDataTlsa").hide(); $("#divAddEditRecordDataSvcb").hide(); $("#divAddEditRecordDataUri").hide(); $("#divAddEditRecordDataCaa").hide(); $("#divAddEditRecordDataForwarder").hide(); $("#divAddEditRecordDataApplication").hide(); switch (type) { case "A": $("#lblAddEditRecordDataValue").text("IPv4 Address"); $("#txtAddEditRecordDataValue").val(""); $("#chkAddEditRecordDataPtr").prop("checked", false); $("#chkAddEditRecordDataCreatePtrZone").prop('disabled', true); $("#chkAddEditRecordDataCreatePtrZone").prop("checked", false); $("#chkAddEditRecordDataPtrLabel").text("Add reverse (PTR) record"); $("#divAddEditRecordData").show(); $("#divAddEditRecordDataPtr").show(); break; case "AAAA": $("#lblAddEditRecordDataValue").text("IPv6 Address"); $("#txtAddEditRecordDataValue").val(""); $("#chkAddEditRecordDataPtr").prop("checked", false); $("#chkAddEditRecordDataCreatePtrZone").prop('disabled', true); $("#chkAddEditRecordDataCreatePtrZone").prop("checked", false); $("#chkAddEditRecordDataPtrLabel").text("Add reverse (PTR) record"); $("#divAddEditRecordData").show(); $("#divAddEditRecordDataPtr").show(); break; case "NS": $("#txtAddEditRecordDataNsNameServer").val(""); $("#txtAddEditRecordDataNsGlue").val(""); $("#divAddEditRecordDataNs").show(); $("#txtAddEditRecordTtl").attr("placeholder", sessionData.info.defaultNsRecordTtl); $("#spanAddEditRecordTtlUnit").text("seconds (default " + sessionData.info.defaultNsRecordTtl + ")"); break; case "SOA": $("#txtEditRecordDataSoaPrimaryNameServer").val(""); $("#txtEditRecordDataSoaResponsiblePerson").val(""); $("#txtEditRecordDataSoaSerial").val(""); $("#txtEditRecordDataSoaRefresh").val(""); $("#txtEditRecordDataSoaRetry").val(""); $("#txtEditRecordDataSoaExpire").val(""); $("#txtEditRecordDataSoaMinimum").val(""); $("#divEditRecordDataSoa").show(); $("#txtAddEditRecordTtl").attr("placeholder", sessionData.info.defaultSoaRecordTtl); $("#spanAddEditRecordTtlUnit").text("seconds (default " + sessionData.info.defaultSoaRecordTtl + ")"); break; case "PTR": case "CNAME": case "DNAME": case "ANAME": $("#lblAddEditRecordDataValue").text("Domain Name"); $("#txtAddEditRecordDataValue").val(""); $("#divAddEditRecordData").show(); break; case "MX": $("#txtAddEditRecordDataMxPreference").val(""); $("#txtAddEditRecordDataMxExchange").val(""); $("#divAddEditRecordDataMx").show(); break; case "TXT": $("#txtAddEditRecordDataTxt").val(""); $("#chkAddEditRecordDataTxtSplitText").prop("checked", false); $("#divAddEditRecordDataTxt").show(); break; case "RP": $("#txtAddEditRecordDataRpMailbox").val(""); $("#txtAddEditRecordDataRpTxtDomain").val(""); $("#divAddEditRecordDataRp").show(); break; case "SRV": $("#txtAddEditRecordName").prop("placeholder", "_service._protocol.name"); $("#txtAddEditRecordDataSrvPriority").val(""); $("#txtAddEditRecordDataSrvWeight").val(""); $("#txtAddEditRecordDataSrvPort").val(""); $("#txtAddEditRecordDataSrvTarget").val(""); $("#divAddEditRecordDataSrv").show(); break; case "NAPTR": $("#txtAddEditRecordDataNaptrOrder").val(""); $("#txtAddEditRecordDataNaptrPreference").val(""); $("#txtAddEditRecordDataNaptrFlags").val(""); $("#txtAddEditRecordDataNaptrServices").val(""); $("#txtAddEditRecordDataNaptrRegExp").val(""); $("#txtAddEditRecordDataNaptrReplacement").val(""); $("#divAddEditRecordDataNaptr").show(); break; case "DS": $("#txtAddEditRecordDataDsKeyTag").val(""); $("#optAddEditRecordDataDsAlgorithm").val(""); $("#optAddEditRecordDataDsDigestType").val(""); $("#txtAddEditRecordDataDsDigest").val(""); $("#divAddEditRecordDataDs").show(); break; case "SSHFP": $("#optAddEditRecordDataSshfpAlgorithm").val(""); $("#optAddEditRecordDataSshfpFingerprintType").val(""); $("#txtAddEditRecordDataSshfpFingerprint").val(""); $("#divAddEditRecordDataSshfp").show(); break; case "TLSA": $("#txtAddEditRecordName").prop("placeholder", "_port._protocol.name"); $("#optAddEditRecordDataTlsaCertificateUsage").val(""); $("#optAddEditRecordDataTlsaSelector").val(""); $("#optAddEditRecordDataTlsaMatchingType").val(""); $("#txtAddEditRecordDataTlsaCertificateAssociationData").val(""); $("#divAddEditRecordDataTlsa").show(); break; case "SVCB": case "HTTPS": $("#txtAddEditRecordName").prop("placeholder", "_port._scheme.name"); $("#txtAddEditRecordDataSvcbPriority").val(""); $("#txtAddEditRecordDataSvcbTargetName").val(""); $("#tableAddEditRecordDataSvcbParams").html(""); $("#chkAddEditRecordDataSvcbAutoIpv4Hint").prop("checked", false); $("#chkAddEditRecordDataSvcbAutoIpv6Hint").prop("checked", false); $("#divAddEditRecordDataSvcb").show(); break; case "URI": $("#txtAddEditRecordDataUriPriority").val(""); $("#txtAddEditRecordDataUriWeight").val(""); $("#txtAddEditRecordDataUri").val(""); $("#divAddEditRecordDataUri").show(); break; case "CAA": $("#txtAddEditRecordDataCaaFlags").val(""); $("#txtAddEditRecordDataCaaTag").val(""); $("#txtAddEditRecordDataCaaValue").val(""); $("#divAddEditRecordDataCaa").show(); break; case "FWD": $("#txtAddEditRecordTtl").prop("disabled", true); $("#txtAddEditRecordTtl").val("0"); $("input[name=rdAddEditRecordDataForwarderProtocol]:radio").attr("disabled", false); $("#rdAddEditRecordDataForwarderProtocolUdp").prop("checked", true); $("#chkAddEditRecordDataForwarderThisServer").prop("checked", false); $("#txtAddEditRecordDataForwarder").prop("disabled", false); $("#txtAddEditRecordDataForwarder").val(""); $("#txtAddEditRecordDataForwarderPriority").val(""); $("#chkAddEditRecordDataForwarderDnssecValidation").prop("checked", $("#chkDnssecValidation").prop("checked")); $("#rdAddEditRecordDataForwarderProxyTypeDefaultProxy").prop("checked", true); $("#txtAddEditRecordDataForwarderProxyAddress").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyPort").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyUsername").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyPassword").prop("disabled", true); $("#txtAddEditRecordDataForwarderProxyAddress").val(""); $("#txtAddEditRecordDataForwarderProxyPort").val(""); $("#txtAddEditRecordDataForwarderProxyUsername").val(""); $("#txtAddEditRecordDataForwarderProxyPassword").val(""); $("#divAddEditRecordDataForwarder").show(); $("#divAddEditRecordDataForwarderProxy").show(); break; case "APP": $("#optAddEditRecordDataAppName").val(""); $("#optAddEditRecordDataClassPath").val(""); $("#txtAddEditRecordDataData").val(""); $("#divAddEditRecordDataApplication").show(); if (addMode) loadAddRecordModalAppNames(); break; default: $("#txtAddEditRecordDataUnknownType").val(""); $("#lblAddEditRecordDataValue").text("RDATA"); $("#txtAddEditRecordDataValue").val(""); $("#txtAddEditRecordDataValue").attr("placeholder", "hex string"); $("#divAddEditRecordData").show(); $("#divAddEditRecordDataUnknownType").show(); break; } } function zoneHasSvcbAutoHint(ipv4, ipv6) { if (editZoneRecords == null) return true; for (var i = 0; i < editZoneRecords.length; i++) { switch (editZoneRecords[i].type) { case "SVCB": case "HTTPS": if ((editZoneRecords[i].rData.autoIpv4Hint && ipv4) || (editZoneRecords[i].rData.autoIpv6Hint && ipv6)) return true; break; } } return false; } function addRecord() { var btn = $("#btnAddEditRecord"); var divAddEditRecordAlert = $("#divAddEditRecordAlert"); var zone = $("#titleEditZone").attr("data-zone"); var domain; { var subDomain = $("#txtAddEditRecordName").val(); if (subDomain === "") subDomain = "@"; if (subDomain === "@") domain = zone; else if (zone === ".") domain = subDomain + "."; else domain = subDomain + "." + zone; } var type = $("#optAddEditRecordType").val(); var ttl = $("#txtAddEditRecordTtl").val(); var overwrite = $("#chkAddEditRecordOverwrite").prop("checked"); var comments = $("#txtAddEditRecordComments").val(); var expiryTtl = $("#txtAddEditRecordExpiryTtl").val(); var apiUrl = ""; switch (type) { case "A": case "AAAA": var ipAddress = $("#txtAddEditRecordDataValue").val(); if (ipAddress === "") { showAlert("warning", "Missing!", "Please enter an IP address to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } var updateSvcbHints = zoneHasSvcbAutoHint(type == "A", type == "AAAA"); apiUrl += "&ipAddress=" + encodeURIComponent(ipAddress) + "&ptr=" + $("#chkAddEditRecordDataPtr").prop('checked') + "&createPtrZone=" + $("#chkAddEditRecordDataCreatePtrZone").prop('checked') + "&updateSvcbHints=" + updateSvcbHints; break; case "NS": var nameServer = $("#txtAddEditRecordDataNsNameServer").val(); if (nameServer === "") { showAlert("warning", "Missing!", "Please enter a name server to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataNsNameServer").trigger("focus"); return; } var glue = cleanTextList($("#txtAddEditRecordDataNsGlue").val()); apiUrl += "&nameServer=" + encodeURIComponent(nameServer) + "&glue=" + encodeURIComponent(glue); break; case "CNAME": var subDomainName = $("#txtAddEditRecordName").val(); if ((subDomainName === "") || (subDomainName === "@")) { showAlert("warning", "Missing!", "Please enter a name for the CNAME record since DNS protocol does not allow CNAME at zone's apex. If you need CNAME like function at the zone's apex then use ANAME record instead.", divAddEditRecordAlert); $("#txtAddEditRecordName").trigger("focus"); return; } var cname = $("#txtAddEditRecordDataValue").val(); if (cname === "") { showAlert("warning", "Missing!", "Please enter a domain name to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&cname=" + encodeURIComponent(cname); break; case "PTR": var ptrName = $("#txtAddEditRecordDataValue").val(); if (ptrName === "") { showAlert("warning", "Missing!", "Please enter a suitable value to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&ptrName=" + encodeURIComponent(ptrName); break; case "MX": var preference = $("#txtAddEditRecordDataMxPreference").val(); if (preference === "") preference = 1; var exchange = $("#txtAddEditRecordDataMxExchange").val(); if (exchange === "") { showAlert("warning", "Missing!", "Please enter a mail exchange domain name to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataMxExchange").trigger("focus"); return; } apiUrl += "&preference=" + preference + "&exchange=" + encodeURIComponent(exchange); break; case "TXT": var text = $("#txtAddEditRecordDataTxt").val(); if (text === "") { showAlert("warning", "Missing!", "Please enter a suitable value to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataTxt").trigger("focus"); return; } var splitText = $("#chkAddEditRecordDataTxtSplitText").prop("checked"); apiUrl += "&text=" + encodeURIComponent(text) + "&splitText=" + splitText; break; case "RP": var mailbox = $("#txtAddEditRecordDataRpMailbox").val(); if (mailbox === "") mailbox = "."; var txtDomain = $("#txtAddEditRecordDataRpTxtDomain").val(); if (txtDomain === "") txtDomain = "."; apiUrl += "&mailbox=" + encodeURIComponent(mailbox) + "&txtDomain=" + encodeURIComponent(txtDomain); break; case "SRV": if ($("#txtAddEditRecordName").val() === "") { showAlert("warning", "Missing!", "Please enter a name that includes service and protocol labels.", divAddEditRecordAlert); $("#txtAddEditRecordName").trigger("focus"); return; } var priority = $("#txtAddEditRecordDataSrvPriority").val(); if (priority === "") { showAlert("warning", "Missing!", "Please enter a suitable priority.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvPriority").trigger("focus"); return; } var weight = $("#txtAddEditRecordDataSrvWeight").val(); if (weight === "") { showAlert("warning", "Missing!", "Please enter a suitable weight.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvWeight").trigger("focus"); return; } var port = $("#txtAddEditRecordDataSrvPort").val(); if (port === "") { showAlert("warning", "Missing!", "Please enter a suitable port number.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvPort").trigger("focus"); return; } var target = $("#txtAddEditRecordDataSrvTarget").val(); if (target === "") { showAlert("warning", "Missing!", "Please enter a suitable value into the target field.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvTarget").trigger("focus"); return; } apiUrl += "&priority=" + priority + "&weight=" + weight + "&port=" + port + "&target=" + encodeURIComponent(target); break; case "NAPTR": var order = $("#txtAddEditRecordDataNaptrOrder").val(); if (order === "") { showAlert("warning", "Missing!", "Please enter a suitable order.", divAddEditRecordAlert); $("#txtAddEditRecordDataNaptrOrder").trigger("focus"); return; } var preference = $("#txtAddEditRecordDataNaptrPreference").val(); if (preference === "") { showAlert("warning", "Missing!", "Please enter a suitable preference.", divAddEditRecordAlert); $("#txtAddEditRecordDataNaptrPreference").trigger("focus"); return; } var flags = $("#txtAddEditRecordDataNaptrFlags").val(); var services = $("#txtAddEditRecordDataNaptrServices").val(); var regexp = $("#txtAddEditRecordDataNaptrRegExp").val(); var replacement = $("#txtAddEditRecordDataNaptrReplacement").val(); apiUrl += "&naptrOrder=" + order + "&naptrPreference=" + preference + "&naptrFlags=" + encodeURIComponent(flags) + "&naptrServices=" + encodeURIComponent(services) + "&naptrRegexp=" + encodeURIComponent(regexp) + "&naptrReplacement=" + encodeURIComponent(replacement); break; case "DNAME": var dname = $("#txtAddEditRecordDataValue").val(); if (dname === "") { showAlert("warning", "Missing!", "Please enter a domain name to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&dname=" + encodeURIComponent(dname); break; case "DS": var subDomainName = $("#txtAddEditRecordName").val(); if ((subDomainName === "") || (subDomainName === "@")) { showAlert("warning", "Missing!", "Please enter a name for the DS record.", divAddEditRecordAlert); $("#txtAddEditRecordName").trigger("focus"); return; } var keyTag = $("#txtAddEditRecordDataDsKeyTag").val(); if (keyTag === "") { showAlert("warning", "Missing!", "Please enter the Key Tag value to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataDsKeyTag").trigger("focus"); return; } var algorithm = $("#optAddEditRecordDataDsAlgorithm").val(); if ((algorithm === null) || (algorithm === "")) { showAlert("warning", "Missing!", "Please select an DNSSEC algorithm to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataDsAlgorithm").trigger("focus"); return; } var digestType = $("#optAddEditRecordDataDsDigestType").val(); if ((digestType === null) || (digestType === "")) { showAlert("warning", "Missing!", "Please select a Digest Type to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataDsDigestType").trigger("focus"); return; } var digest = $("#txtAddEditRecordDataDsDigest").val(); if (digest === "") { showAlert("warning", "Missing!", "Please enter the Digest hash in hex string format to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataDsDigest").trigger("focus"); return; } apiUrl += "&keyTag=" + keyTag + "&algorithm=" + algorithm + "&digestType=" + digestType + "&digest=" + encodeURIComponent(digest); break; case "SSHFP": var sshfpAlgorithm = $("#optAddEditRecordDataSshfpAlgorithm").val(); if ((sshfpAlgorithm === null) || (sshfpAlgorithm === "")) { showAlert("warning", "Missing!", "Please select an Algorithm to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataSshfpAlgorithm").trigger("focus"); return; } var sshfpFingerprintType = $("#optAddEditRecordDataSshfpFingerprintType").val(); if ((sshfpFingerprintType === null) || (sshfpFingerprintType === "")) { showAlert("warning", "Missing!", "Please select a Fingerprint Type to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataSshfpFingerprintType").trigger("focus"); return; } var sshfpFingerprint = $("#txtAddEditRecordDataSshfpFingerprint").val(); if (sshfpFingerprint === "") { showAlert("warning", "Missing!", "Please enter the Fingerprint hash in hex string format to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataSshfpFingerprint").trigger("focus"); return; } apiUrl += "&sshfpAlgorithm=" + sshfpAlgorithm + "&sshfpFingerprintType=" + sshfpFingerprintType + "&sshfpFingerprint=" + encodeURIComponent(sshfpFingerprint); break; case "TLSA": var tlsaCertificateUsage = $("#optAddEditRecordDataTlsaCertificateUsage").val(); if ((tlsaCertificateUsage === null) || (tlsaCertificateUsage === "")) { showAlert("warning", "Missing!", "Please select a Certificate Usage to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataTlsaCertificateUsage").trigger("focus"); return; } var tlsaSelector = $("#optAddEditRecordDataTlsaSelector").val(); if ((tlsaSelector === null) || (tlsaSelector === "")) { showAlert("warning", "Missing!", "Please select a Selector to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataTlsaSelector").trigger("focus"); return; } var tlsaMatchingType = $("#optAddEditRecordDataTlsaMatchingType").val(); if ((tlsaMatchingType === null) || (tlsaMatchingType === "")) { showAlert("warning", "Missing!", "Please select a Matching Type to add the record.", divAddEditRecordAlert); $("#optAddEditRecordDataTlsaMatchingType").trigger("focus"); return; } var tlsaCertificateAssociationData = $("#txtAddEditRecordDataTlsaCertificateAssociationData").val(); if (tlsaCertificateAssociationData === "") { showAlert("warning", "Missing!", "Please enter the Certificate Association Data to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataTlsaCertificateAssociationData").trigger("focus"); return; } if ((tlsaMatchingType === "Full") && !tlsaCertificateAssociationData.startsWith("-")) { showAlert("warning", "Missing!", "Please enter a complete certificate in PEM format as the Certificate Association Data to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataTlsaCertificateAssociationData").trigger("focus"); return; } apiUrl += "&tlsaCertificateUsage=" + tlsaCertificateUsage + "&tlsaSelector=" + tlsaSelector + "&tlsaMatchingType=" + tlsaMatchingType + "&tlsaCertificateAssociationData=" + encodeURIComponent(tlsaCertificateAssociationData); break; case "SVCB": case "HTTPS": var svcPriority = $("#txtAddEditRecordDataSvcbPriority").val(); if ((svcPriority === null) || (svcPriority === "")) { showAlert("warning", "Missing!", "Please enter a Priority value to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataSvcbPriority").trigger("focus"); return; } var svcTargetName = $("#txtAddEditRecordDataSvcbTargetName").val(); if ((svcTargetName === null) || (svcTargetName === "")) { showAlert("warning", "Missing!", "Please enter a Target Name to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataSvcbTargetName").trigger("focus"); return; } var svcParams = serializeTableData($("#tableAddEditRecordDataSvcbParams"), 2, divAddEditRecordAlert); if (svcParams === false) return; if (svcParams.length === 0) svcParams = false; var autoIpv4Hint = $("#chkAddEditRecordDataSvcbAutoIpv4Hint").prop("checked"); var autoIpv6Hint = $("#chkAddEditRecordDataSvcbAutoIpv6Hint").prop("checked"); apiUrl += "&svcPriority=" + svcPriority + "&svcTargetName=" + encodeURIComponent(svcTargetName) + "&svcParams=" + encodeURIComponent(svcParams) + "&autoIpv4Hint=" + autoIpv4Hint + "&autoIpv6Hint=" + autoIpv6Hint; break; case "URI": var uriPriority = $("#txtAddEditRecordDataUriPriority").val(); if (uriPriority === "") { showAlert("warning", "Missing!", "Please enter a suitable priority.", divAddEditRecordAlert); $("#txtAddEditRecordDataUriPriority").trigger("focus"); return; } var uriWeight = $("#txtAddEditRecordDataUriWeight").val(); if (uriWeight === "") { showAlert("warning", "Missing!", "Please enter a suitable weight.", divAddEditRecordAlert); $("#txtAddEditRecordDataUriWeight").trigger("focus"); return; } var uri = $("#txtAddEditRecordDataUri").val(); if (uri === "") { showAlert("warning", "Missing!", "Please enter a suitable value into the URI field.", divAddEditRecordAlert); $("#txtAddEditRecordDataUri").trigger("focus"); return; } apiUrl += "&uriPriority=" + uriPriority + "&uriWeight=" + uriWeight + "&uri=" + encodeURIComponent(uri); break; case "CAA": var flags = $("#txtAddEditRecordDataCaaFlags").val(); if (flags === "") flags = 0; var tag = $("#txtAddEditRecordDataCaaTag").val(); if (tag === "") tag = "issue"; var value = $("#txtAddEditRecordDataCaaValue").val(); if (value === "") { showAlert("warning", "Missing!", "Please enter a suitable value into the authority field.", divAddEditRecordAlert); $("#txtAddEditRecordDataCaaValue").trigger("focus"); return; } apiUrl += "&flags=" + flags + "&tag=" + encodeURIComponent(tag) + "&value=" + encodeURIComponent(value); break; case "ANAME": var aname = $("#txtAddEditRecordDataValue").val(); if (aname === "") { showAlert("warning", "Missing!", "Please enter a suitable value to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&aname=" + encodeURIComponent(aname); break; case "FWD": var forwarder = $("#txtAddEditRecordDataForwarder").val(); if (forwarder === "") { showAlert("warning", "Missing!", "Please enter a domain name or IP address or URL as a forwarder to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataForwarder").trigger("focus"); return; } var forwarderPriority = $("#txtAddEditRecordDataForwarderPriority").val(); var dnssecValidation = $("#chkAddEditRecordDataForwarderDnssecValidation").prop("checked"); var proxyType = $("input[name=rdAddEditRecordDataForwarderProxyType]:checked").val(); apiUrl += "&protocol=" + $('input[name=rdAddEditRecordDataForwarderProtocol]:checked').val() + "&forwarder=" + encodeURIComponent(forwarder); apiUrl += "&forwarderPriority=" + forwarderPriority + "&dnssecValidation=" + dnssecValidation + "&proxyType=" + proxyType; switch (proxyType) { case "Http": case "Socks5": var proxyAddress = $("#txtAddEditRecordDataForwarderProxyAddress").val(); var proxyPort = $("#txtAddEditRecordDataForwarderProxyPort").val(); var proxyUsername = $("#txtAddEditRecordDataForwarderProxyUsername").val(); var proxyPassword = $("#txtAddEditRecordDataForwarderProxyPassword").val(); if ((proxyAddress == null) || (proxyAddress === "")) { showAlert("warning", "Missing!", "Please enter a domain name or IP address for Proxy Server Address to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataForwarderProxyAddress").trigger("focus"); return; } if ((proxyPort == null) || (proxyPort === "")) { showAlert("warning", "Missing!", "Please enter a port number for Proxy Server Port to add the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataForwarderProxyPort").trigger("focus"); return; } apiUrl += "&proxyAddress=" + encodeURIComponent(proxyAddress) + "&proxyPort=" + proxyPort + "&proxyUsername=" + encodeURIComponent(proxyUsername) + "&proxyPassword=" + encodeURIComponent(proxyPassword); break; } break; case "APP": var appName = $("#optAddEditRecordDataAppName").val(); if ((appName === null) || (appName === "")) { showAlert("warning", "Missing!", "Please select an application name to add record.", divAddEditRecordAlert); $("#optAddEditRecordDataAppName").trigger("focus"); return; } var classPath = $("#optAddEditRecordDataClassPath").val(); if ((classPath === null) || (classPath === "")) { showAlert("warning", "Missing!", "Please select a class path to add record.", divAddEditRecordAlert); $("#optAddEditRecordDataClassPath").trigger("focus"); return; } var recordData = $("#txtAddEditRecordDataData").val(); apiUrl += "&appName=" + encodeURIComponent(appName) + "&classPath=" + encodeURIComponent(classPath) + "&recordData=" + encodeURIComponent(recordData); break; default: type = $("#txtAddEditRecordDataUnknownType").val(); if ((type === null) || (type === "")) { showAlert("warning", "Missing!", "Please enter a resoure record name or number to add record.", divAddEditRecordAlert); $("#txtAddEditRecordDataUnknownType").trigger("focus"); return; } var rdata = $("#txtAddEditRecordDataValue").val(); if ((rdata === null) || (rdata === "")) { showAlert("warning", "Missing!", "Please enter a hex value as the RDATA to add record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&rdata=" + encodeURIComponent(rdata); break; } var node = $("#optZonesClusterNode").val(); apiUrl = "api/zones/records/add?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&domain=" + encodeURIComponent(domain) + "&type=" + encodeURIComponent(type) + "&ttl=" + ttl + "&overwrite=" + overwrite + "&comments=" + encodeURIComponent(comments) + "&expiryTtl=" + expiryTtl + apiUrl; btn.button("loading"); HTTPRequest({ url: apiUrl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#modalAddEditRecord").modal("hide"); if (overwrite) { var currentPageNumber = Number($("#txtEditZonePageNumber").val()); showEditZone(zone, currentPageNumber); } else { //update local array editZoneRecords.unshift(responseJSON.response.addedRecord); editZoneFilteredRecords = null; //to evaluate filters again //show page showEditZonePage(1); } showAlert("success", "Record Added!", "Resource record was added successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalAddEditRecord").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divAddEditRecordAlert }); } function updateAddEditFormForwarderPlaceholder() { var protocol = $('input[name=rdAddEditRecordDataForwarderProtocol]:checked').val(); switch (protocol) { case "Udp": case "Tcp": $("#txtAddEditRecordDataForwarder").attr("placeholder", "8.8.8.8 or [2620:fe::10]") break; case "Tls": case "Quic": $("#txtAddEditRecordDataForwarder").attr("placeholder", "dns.quad9.net (9.9.9.9:853)") break; case "Https": $("#txtAddEditRecordDataForwarder").attr("placeholder", "https://cloudflare-dns.com/dns-query (1.1.1.1)") break; } } function updateAddEditFormForwarderProxyType() { var proxyType = $('input[name=rdAddEditRecordDataForwarderProxyType]:checked').val(); var disabled = (proxyType === "NoProxy") || (proxyType === "DefaultProxy"); $("#txtAddEditRecordDataForwarderProxyAddress").prop("disabled", disabled); $("#txtAddEditRecordDataForwarderProxyPort").prop("disabled", disabled); $("#txtAddEditRecordDataForwarderProxyUsername").prop("disabled", disabled); $("#txtAddEditRecordDataForwarderProxyPassword").prop("disabled", disabled); } function updateAddEditFormForwarderThisServer() { var useThisServer = $("#chkAddEditRecordDataForwarderThisServer").prop('checked'); if (useThisServer) { $("input[name=rdAddEditRecordDataForwarderProtocol]:radio").attr("disabled", true); $("#rdAddEditRecordDataForwarderProtocolUdp").prop("checked", true); $("#txtAddEditRecordDataForwarder").attr("placeholder", "8.8.8.8 or [2620:fe::10]") $("#txtAddEditRecordDataForwarder").prop("disabled", true); $("#txtAddEditRecordDataForwarder").val("this-server"); $("#divAddEditRecordDataForwarderProxy").hide(); } else { $("input[name=rdAddEditRecordDataForwarderProtocol]:radio").attr("disabled", false); $("#txtAddEditRecordDataForwarder").prop("disabled", false); $("#txtAddEditRecordDataForwarder").val(""); $("#divAddEditRecordDataForwarderProxy").show(); } } function addSvcbRecordParamEditRow(paramKey, paramValue) { var id = Math.floor(Math.random() * 10000); var tableHtmlRows = ""; if ((paramKey != "") && isFinite(paramKey)) { tableHtmlRows += ""; tableHtmlRows += ""; } else { tableHtmlRows += ""; tableHtmlRows += "'); }\">"; tableHtmlRows += "mandatory"; tableHtmlRows += "alpn"; tableHtmlRows += "no-default-alpn"; tableHtmlRows += "port"; tableHtmlRows += "ipv4hint"; tableHtmlRows += "ipv6hint"; tableHtmlRows += "dohpath"; tableHtmlRows += ""; tableHtmlRows += ""; tableHtmlRows += ""; } tableHtmlRows += ""; $("#tableAddEditRecordDataSvcbParams").append(tableHtmlRows); } function showEditRecordModal(objBtn) { var btn = $(objBtn); var id = btn.attr("data-id"); var divData = $("#data" + id); var zone = $("#titleEditZone").attr("data-zone"); var zoneType = $("#titleEditZone").attr("data-zone-type"); var catalogZone = $("#titleEditZoneCatalog").text(); var name = divData.attr("data-record-name"); var type = divData.attr("data-record-type"); var ttl = divData.attr("data-record-ttl"); var comments = divData.attr("data-record-comments"); var expiryTtl = divData.attr("data-record-expiry-ttl"); if (name === zone) name = "@"; else name = name.replace("." + zone, ""); clearAddEditRecordForm(); $("#titleAddEditRecord").text("Edit Record"); $("#lblAddEditRecordZoneName").text(zone === "." ? "" : zone); $("#optEditRecordTypeSoa").show(); $("#optAddEditRecordType").val(type); $("#divAddEditRecordOverwrite").hide(); modifyAddRecordFormByType(false); $("#txtAddEditRecordName").val(name); $("#txtAddEditRecordTtl").val(ttl) $("#txtAddEditRecordComments").val(comments); $("#txtAddEditRecordExpiryTtl").val(expiryTtl); switch (type) { case "A": case "AAAA": $("#txtAddEditRecordDataValue").val(divData.attr("data-record-ip-address")); $("#chkAddEditRecordDataPtr").prop("checked", false); $("#chkAddEditRecordDataCreatePtrZone").prop("disabled", true); $("#chkAddEditRecordDataCreatePtrZone").prop("checked", false); $("#chkAddEditRecordDataPtrLabel").text("Update reverse (PTR) record"); break; case "NS": if ((zoneType == "Primary") && (name == "@") && sessionData.info.clusterInitialized && (catalogZone == "cluster-catalog." + sessionData.info.clusterDomain)) { $("#txtAddEditRecordName").prop("disabled", true); $("#txtAddEditRecordDataNsNameServer").prop("disabled", true); $("#txtAddEditRecordDataNsGlue").prop("disabled", true); $("#txtAddEditRecordExpiryTtl").prop("disabled", true); } $("#txtAddEditRecordDataNsNameServer").val(divData.attr("data-record-name-server")); $("#txtAddEditRecordDataNsGlue").val(divData.attr("data-record-glue").replace(/, /g, "\n")); break; case "CNAME": $("#txtAddEditRecordDataValue").val(divData.attr("data-record-cname")); break; case "SOA": $("#txtEditRecordDataSoaPrimaryNameServer").val(divData.attr("data-record-pname")); $("#txtEditRecordDataSoaResponsiblePerson").val(divData.attr("data-record-rperson")); $("#txtEditRecordDataSoaSerial").val(divData.attr("data-record-serial")); $("#txtEditRecordDataSoaSerial").prop("disabled", divData.attr("data-record-serial-scheme") === "true"); $("#txtEditRecordDataSoaRefresh").val(divData.attr("data-record-refresh")); $("#txtEditRecordDataSoaRetry").val(divData.attr("data-record-retry")); $("#txtEditRecordDataSoaExpire").val(divData.attr("data-record-expire")); $("#txtEditRecordDataSoaMinimum").val(divData.attr("data-record-minimum")); $("#chkEditRecordDataSoaUseSerialDateScheme").prop("checked", divData.attr("data-record-serial-scheme") === "true"); $("#txtAddEditRecordName").prop("disabled", true); $("#divAddEditRecordExpiryTtl").hide(); switch (zoneType) { case "Primary": if (sessionData.info.clusterInitialized && (catalogZone == "cluster-catalog." + sessionData.info.clusterDomain)) $("#txtEditRecordDataSoaPrimaryNameServer").prop("disabled", true); else $("#txtEditRecordDataSoaPrimaryNameServer").prop("disabled", false); $("#txtAddEditRecordTtl").prop("disabled", false); $("#txtEditRecordDataSoaResponsiblePerson").prop("disabled", false); break; case "Forwarder": $("#txtAddEditRecordTtl").prop("disabled", true); $("#txtEditRecordDataSoaResponsiblePerson").prop("disabled", true); break; case "Catalog": $("#txtAddEditRecordTtl").prop("disabled", true); $("#txtEditRecordDataSoaPrimaryNameServer").prop("disabled", true); $("#txtEditRecordDataSoaResponsiblePerson").prop("disabled", true); break; default: $("#txtAddEditRecordTtl").prop("disabled", false); $("#txtEditRecordDataSoaPrimaryNameServer").prop("disabled", false); $("#txtEditRecordDataSoaResponsiblePerson").prop("disabled", false); break; } break; case "PTR": $("#txtAddEditRecordDataValue").val(divData.attr("data-record-ptr-name")); break; case "MX": $("#txtAddEditRecordDataMxPreference").val(divData.attr("data-record-preference")); $("#txtAddEditRecordDataMxExchange").val(divData.attr("data-record-exchange")); break; case "TXT": $("#txtAddEditRecordDataTxt").val(divData.attr("data-record-text")); $("#chkAddEditRecordDataTxtSplitText").prop("checked", divData.attr("data-record-split-text") === "true"); break; case "RP": $("#txtAddEditRecordDataRpMailbox").val(divData.attr("data-record-mailbox")); $("#txtAddEditRecordDataRpTxtDomain").val(divData.attr("data-record-txt-domain")); break; case "SRV": $("#txtAddEditRecordDataSrvPriority").val(divData.attr("data-record-priority")); $("#txtAddEditRecordDataSrvWeight").val(divData.attr("data-record-weight")); $("#txtAddEditRecordDataSrvPort").val(divData.attr("data-record-port")); $("#txtAddEditRecordDataSrvTarget").val(divData.attr("data-record-target")); break; case "NAPTR": $("#txtAddEditRecordDataNaptrOrder").val(divData.attr("data-record-order")); $("#txtAddEditRecordDataNaptrPreference").val(divData.attr("data-record-preference")); $("#txtAddEditRecordDataNaptrFlags").val(divData.attr("data-record-flags")); $("#txtAddEditRecordDataNaptrServices").val(divData.attr("data-record-services")); $("#txtAddEditRecordDataNaptrRegExp").val(divData.attr("data-record-regexp")); $("#txtAddEditRecordDataNaptrReplacement").val(divData.attr("data-record-replacement")); break; case "DNAME": $("#txtAddEditRecordDataValue").val(divData.attr("data-record-dname")); break; case "DS": $("#txtAddEditRecordDataDsKeyTag").val(divData.attr("data-record-key-tag")); $("#optAddEditRecordDataDsAlgorithm").val(divData.attr("data-record-algorithm")); $("#optAddEditRecordDataDsDigestType").val(divData.attr("data-record-digest-type")); $("#txtAddEditRecordDataDsDigest").val(divData.attr("data-record-digest")); break; case "SSHFP": $("#optAddEditRecordDataSshfpAlgorithm").val(divData.attr("data-record-algorithm")); $("#optAddEditRecordDataSshfpFingerprintType").val(divData.attr("data-record-fingerprint-type")); $("#txtAddEditRecordDataSshfpFingerprint").val(divData.attr("data-record-fingerprint")); break; case "TLSA": $("#optAddEditRecordDataTlsaCertificateUsage").val(divData.attr("data-record-certificate-usage")); $("#optAddEditRecordDataTlsaSelector").val(divData.attr("data-record-selector")); $("#optAddEditRecordDataTlsaMatchingType").val(divData.attr("data-record-matching-type")); $("#txtAddEditRecordDataTlsaCertificateAssociationData").val(divData.attr("data-record-certificate-association-data")); break; case "SVCB": case "HTTPS": $("#txtAddEditRecordDataSvcbPriority").val(divData.attr("data-record-svc-priority")); $("#txtAddEditRecordDataSvcbTargetName").val(divData.attr("data-record-svc-target-name")); var svcParams = JSON.parse(divData.attr("data-record-svc-params")); var autoIpv4Hint = divData.attr("data-record-auto-ipv4hint") === "true"; var autoIpv6Hint = divData.attr("data-record-auto-ipv6hint") === "true"; for (var paramKey in svcParams) { switch (paramKey) { case "ipv4hint": if (autoIpv4Hint) continue; break; case "ipv6hint": if (autoIpv6Hint) continue; break; } addSvcbRecordParamEditRow(paramKey, svcParams[paramKey]); } $("#chkAddEditRecordDataSvcbAutoIpv4Hint").prop("checked", autoIpv4Hint); $("#chkAddEditRecordDataSvcbAutoIpv6Hint").prop("checked", autoIpv6Hint); break; case "URI": $("#txtAddEditRecordDataUriPriority").val(divData.attr("data-record-priority")); $("#txtAddEditRecordDataUriWeight").val(divData.attr("data-record-weight")); $("#txtAddEditRecordDataUri").val(divData.attr("data-record-uri")); break; case "CAA": $("#txtAddEditRecordDataCaaFlags").val(divData.attr("data-record-flags")); $("#txtAddEditRecordDataCaaTag").val(divData.attr("data-record-tag")); $("#txtAddEditRecordDataCaaValue").val(divData.attr("data-record-value")); break; case "ANAME": $("#txtAddEditRecordDataValue").val(divData.attr("data-record-aname")); break; case "FWD": $("#txtAddEditRecordTtl").prop("disabled", true); $("#rdAddEditRecordDataForwarderProtocol" + divData.attr("data-record-protocol")).prop("checked", true); var forwarder = divData.attr("data-record-forwarder"); $("#chkAddEditRecordDataForwarderThisServer").prop("checked", (forwarder == "this-server")); $("#txtAddEditRecordDataForwarder").prop("disabled", (forwarder == "this-server")); $("#txtAddEditRecordDataForwarder").val(forwarder); if (forwarder === "this-server") { $("input[name=rdAddEditRecordDataForwarderProtocol]:radio").attr("disabled", true); $("#divAddEditRecordDataForwarderProxy").hide(); } else { $("input[name=rdAddEditRecordDataForwarderProtocol]:radio").attr("disabled", false); $("#divAddEditRecordDataForwarderProxy").show(); } $("#txtAddEditRecordDataForwarderPriority").val(divData.attr("data-record-priority")); $("#chkAddEditRecordDataForwarderDnssecValidation").prop("checked", divData.attr("data-record-dnssec-validation") === "true"); var proxyType = divData.attr("data-record-proxy-type"); $("#rdAddEditRecordDataForwarderProxyType" + proxyType).prop("checked", true); switch (proxyType) { case "Http": case "Socks5": $("#txtAddEditRecordDataForwarderProxyAddress").val(divData.attr("data-record-proxy-address")); $("#txtAddEditRecordDataForwarderProxyPort").val(divData.attr("data-record-proxy-port")); $("#txtAddEditRecordDataForwarderProxyUsername").val(divData.attr("data-record-proxy-username")); $("#txtAddEditRecordDataForwarderProxyPassword").val(divData.attr("data-record-proxy-password")); break; } updateAddEditFormForwarderPlaceholder(); updateAddEditFormForwarderProxyType(); break; case "APP": $("#optAddEditRecordDataAppName").prop("disabled", true); $("#optAddEditRecordDataClassPath").prop("disabled", true); $("#optAddEditRecordDataAppName").html("") $("#optAddEditRecordDataAppName").val(divData.attr("data-record-app-name")) $("#optAddEditRecordDataClassPath").html("") $("#optAddEditRecordDataClassPath").val(divData.attr("data-record-classpath")) $("#txtAddEditRecordDataData").val(divData.attr("data-record-data")) break; default: var rdata = divData.attr("data-record-rdata"); if (rdata == null) { showAlert("danger", "Not Supported!", "Editing this record type is not supported."); return; } $("#optAddEditRecordType").val("Unknown"); $("#txtAddEditRecordDataUnknownType").val(type); $("#txtAddEditRecordDataUnknownType").prop("disabled", true); $("#txtAddEditRecordDataValue").val(rdata); break; } $("#optAddEditRecordType").prop("disabled", true); $("#btnAddEditRecord").attr("data-id", id); $("#btnAddEditRecord").attr("onclick", "updateRecord(); return false;"); $("#modalAddEditRecord").modal("show"); setTimeout(function () { $("#txtAddEditRecordName").trigger("focus"); }, 1000); } function updateRecord() { var btn = $("#btnAddEditRecord"); var divAddEditRecordAlert = $("#divAddEditRecordAlert"); var index = Number(btn.attr("data-id")); var divData = $("#data" + index); var zone = $("#titleEditZone").attr("data-zone"); var recordIndex = Number(divData.attr("data-record-index")); var type = divData.attr("data-record-type"); var domain = divData.attr("data-record-name"); if (domain === "") domain = "."; var newDomain; { var newSubDomain = $("#txtAddEditRecordName").val(); if (newSubDomain === "") newSubDomain = "@"; if (newSubDomain === "@") newDomain = zone; else if (zone === ".") newDomain = newSubDomain + "."; else newDomain = newSubDomain + "." + zone; } var ttl = $("#txtAddEditRecordTtl").val(); var disable = (divData.attr("data-record-disabled") === "true"); var comments = $("#txtAddEditRecordComments").val(); var expiryTtl = $("#txtAddEditRecordExpiryTtl").val(); var apiUrl = ""; switch (type) { case "A": case "AAAA": var ipAddress = divData.attr("data-record-ip-address"); var newIpAddress = $("#txtAddEditRecordDataValue").val(); if (newIpAddress === "") { showAlert("warning", "Missing!", "Please enter an IP address to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } var updateSvcbHints = zoneHasSvcbAutoHint(type == "A", type == "AAAA"); apiUrl += "&ipAddress=" + encodeURIComponent(ipAddress) + "&newIpAddress=" + encodeURIComponent(newIpAddress) + "&ptr=" + $("#chkAddEditRecordDataPtr").prop('checked') + "&createPtrZone=" + $("#chkAddEditRecordDataCreatePtrZone").prop('checked') + "&updateSvcbHints=" + updateSvcbHints; break; case "NS": var nameServer = divData.attr("data-record-name-server"); var newNameServer = $("#txtAddEditRecordDataNsNameServer").val(); if (newNameServer === "") { showAlert("warning", "Missing!", "Please enter a name server to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataNsNameServer").trigger("focus"); return; } var glue = cleanTextList($("#txtAddEditRecordDataNsGlue").val()); apiUrl += "&nameServer=" + encodeURIComponent(nameServer) + "&newNameServer=" + encodeURIComponent(newNameServer) + "&glue=" + encodeURIComponent(glue); break; case "CNAME": var subDomainName = $("#txtAddEditRecordName").val(); if ((subDomainName === "") || (subDomainName === "@")) { showAlert("warning", "Missing!", "Please enter a name for the CNAME record since DNS protocol does not allow CNAME at zone's apex. If you need CNAME like function at the zone's apex then use ANAME record instead.", divAddEditRecordAlert); $("#txtAddEditRecordName").trigger("focus"); return; } var cname = $("#txtAddEditRecordDataValue").val(); if (cname === "") { showAlert("warning", "Missing!", "Please enter a domain name to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&cname=" + encodeURIComponent(cname); break; case "SOA": var primaryNameServer = $("#txtEditRecordDataSoaPrimaryNameServer").val(); if (primaryNameServer === "") { showAlert("warning", "Missing!", "Please enter a value for primary name server.", divAddEditRecordAlert); $("#txtEditRecordDataSoaPrimaryNameServer").trigger("focus"); return; } var responsiblePerson = $("#txtEditRecordDataSoaResponsiblePerson").val(); if (responsiblePerson === "") { showAlert("warning", "Missing!", "Please enter a value for responsible person.", divAddEditRecordAlert); $("#txtEditRecordDataSoaResponsiblePerson").trigger("focus"); return; } var serial = $("#txtEditRecordDataSoaSerial").val(); if (serial === "") { showAlert("warning", "Missing!", "Please enter a value for serial.", divAddEditRecordAlert); $("#txtEditRecordDataSoaSerial").trigger("focus"); return; } var refresh = $("#txtEditRecordDataSoaRefresh").val(); if (refresh === "") { showAlert("warning", "Missing!", "Please enter a value for refresh.", divAddEditRecordAlert); $("#txtEditRecordDataSoaRefresh").trigger("focus"); return; } var retry = $("#txtEditRecordDataSoaRetry").val(); if (retry === "") { showAlert("warning", "Missing!", "Please enter a value for retry.", divAddEditRecordAlert); $("#txtEditRecordDataSoaRetry").trigger("focus"); return; } var expire = $("#txtEditRecordDataSoaExpire").val(); if (expire === "") { showAlert("warning", "Missing!", "Please enter a value for expire.", divAddEditRecordAlert); $("#txtEditRecordDataSoaExpire").trigger("focus"); return; } var minimum = $("#txtEditRecordDataSoaMinimum").val(); if (minimum === "") { showAlert("warning", "Missing!", "Please enter a value for minimum.", divAddEditRecordAlert); $("#txtEditRecordDataSoaMinimum").trigger("focus"); return; } var useSerialDateScheme = $("#chkEditRecordDataSoaUseSerialDateScheme").prop("checked"); apiUrl += "&primaryNameServer=" + encodeURIComponent(primaryNameServer) + "&responsiblePerson=" + encodeURIComponent(responsiblePerson) + "&serial=" + encodeURIComponent(serial) + "&refresh=" + encodeURIComponent(refresh) + "&retry=" + encodeURIComponent(retry) + "&expire=" + encodeURIComponent(expire) + "&minimum=" + encodeURIComponent(minimum) + "&useSerialDateScheme=" + encodeURIComponent(useSerialDateScheme); break; case "PTR": var ptrName = divData.attr("data-record-ptr-name"); var newPtrName = $("#txtAddEditRecordDataValue").val(); if (newPtrName === "") { showAlert("warning", "Missing!", "Please enter a suitable value to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&ptrName=" + encodeURIComponent(ptrName) + "&newPtrName=" + encodeURIComponent(newPtrName); break; case "MX": var preference = divData.attr("data-record-preference"); var newPreference = $("#txtAddEditRecordDataMxPreference").val(); if (newPreference === "") newPreference = 1; var exchange = divData.attr("data-record-exchange"); var newExchange = $("#txtAddEditRecordDataMxExchange").val(); if (newExchange === "") { showAlert("warning", "Missing!", "Please enter a mail exchange domain name to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataMxExchange").trigger("focus"); return; } apiUrl += "&preference=" + preference + "&newPreference=" + newPreference + "&exchange=" + encodeURIComponent(exchange) + "&newExchange=" + encodeURIComponent(newExchange); break; case "TXT": var text = divData.attr("data-record-text"); var newText = $("#txtAddEditRecordDataTxt").val(); if (newText === "") { showAlert("warning", "Missing!", "Please enter a suitable value to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataTxt").trigger("focus"); return; } var splitText = divData.attr("data-record-split-text"); var newSplitText = $("#chkAddEditRecordDataTxtSplitText").prop("checked"); apiUrl += "&text=" + encodeURIComponent(text) + "&newText=" + encodeURIComponent(newText) + "&splitText=" + splitText + "&newSplitText=" + newSplitText; break; case "RP": var mailbox = divData.attr("data-record-mailbox"); var newMailbox = $("#txtAddEditRecordDataRpMailbox").val(); if (newMailbox === "") newMailbox = "."; var txtDomain = divData.attr("data-record-txt-domain"); var newTxtDomain = $("#txtAddEditRecordDataRpTxtDomain").val(); if (newTxtDomain === "") newTxtDomain = "."; apiUrl += "&mailbox=" + encodeURIComponent(mailbox) + "&newMailbox=" + encodeURIComponent(newMailbox) + "&txtDomain=" + encodeURIComponent(txtDomain) + "&newTxtDomain=" + encodeURIComponent(newTxtDomain); break; case "SRV": if ($("#txtAddEditRecordName").val() === "") { showAlert("warning", "Missing!", "Please enter a name that includes service and protocol labels.", divAddEditRecordAlert); $("#txtAddEditRecordName").trigger("focus"); return; } var priority = divData.attr("data-record-priority"); var newPriority = $("#txtAddEditRecordDataSrvPriority").val(); if (newPriority === "") { showAlert("warning", "Missing!", "Please enter a suitable priority.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvPriority").trigger("focus"); return; } var weight = divData.attr("data-record-weight"); var newWeight = $("#txtAddEditRecordDataSrvWeight").val(); if (newWeight === "") { showAlert("warning", "Missing!", "Please enter a suitable weight.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvWeight").trigger("focus"); return; } var port = divData.attr("data-record-port"); var newPort = $("#txtAddEditRecordDataSrvPort").val(); if (newPort === "") { showAlert("warning", "Missing!", "Please enter a suitable port number.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvPort").trigger("focus"); return; } var target = divData.attr("data-record-target"); var newTarget = $("#txtAddEditRecordDataSrvTarget").val(); if (newTarget === "") { showAlert("warning", "Missing!", "Please enter a suitable value into the target field.", divAddEditRecordAlert); $("#txtAddEditRecordDataSrvTarget").trigger("focus"); return; } apiUrl += "&priority=" + priority + "&newPriority=" + newPriority + "&weight=" + weight + "&newWeight=" + newWeight + "&port=" + port + "&newPort=" + newPort + "&target=" + encodeURIComponent(target) + "&newTarget=" + encodeURIComponent(newTarget); break; case "NAPTR": var order = divData.attr("data-record-order"); var preference = divData.attr("data-record-preference"); var flags = divData.attr("data-record-flags"); var services = divData.attr("data-record-services"); var regexp = divData.attr("data-record-regexp"); var replacement = divData.attr("data-record-replacement"); var newOrder = $("#txtAddEditRecordDataNaptrOrder").val(); if (newOrder === "") { showAlert("warning", "Missing!", "Please enter a suitable order.", divAddEditRecordAlert); $("#txtAddEditRecordDataNaptrOrder").trigger("focus"); return; } var newPreference = $("#txtAddEditRecordDataNaptrPreference").val(); if (newPreference === "") { showAlert("warning", "Missing!", "Please enter a suitable preference.", divAddEditRecordAlert); $("#txtAddEditRecordDataNaptrPreference").trigger("focus"); return; } var newFlags = $("#txtAddEditRecordDataNaptrFlags").val(); var newServices = $("#txtAddEditRecordDataNaptrServices").val(); var newRegexp = $("#txtAddEditRecordDataNaptrRegExp").val(); var newReplacement = $("#txtAddEditRecordDataNaptrReplacement").val(); if (newReplacement === "") newReplacement = "."; apiUrl += "&naptrOrder=" + order + "&naptrNewOrder=" + newOrder + "&naptrPreference=" + preference + "&naptrNewPreference=" + newPreference + "&naptrFlags=" + encodeURIComponent(flags) + "&naptrNewFlags=" + encodeURIComponent(newFlags) + "&naptrServices=" + encodeURIComponent(services) + "&naptrNewServices=" + encodeURIComponent(newServices) + "&naptrRegexp=" + encodeURIComponent(regexp) + "&naptrNewRegexp=" + encodeURIComponent(newRegexp) + "&naptrReplacement=" + encodeURIComponent(replacement) + "&naptrNewReplacement=" + encodeURIComponent(newReplacement); break; case "DNAME": var dname = $("#txtAddEditRecordDataValue").val(); if (dname === "") { showAlert("warning", "Missing!", "Please enter a domain name to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&dname=" + encodeURIComponent(dname); break; case "DS": var subDomainName = $("#txtAddEditRecordName").val(); if ((subDomainName === "") || (subDomainName === "@")) { showAlert("warning", "Missing!", "Please enter a name for the DS record.", divAddEditRecordAlert); $("#txtAddEditRecordName").trigger("focus"); return; } var keyTag = divData.attr("data-record-key-tag"); var algorithm = divData.attr("data-record-algorithm"); var digestType = divData.attr("data-record-digest-type"); var newKeyTag = $("#txtAddEditRecordDataDsKeyTag").val(); if (newKeyTag === "") { showAlert("warning", "Missing!", "Please enter the Key Tag value to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataDsKeyTag").trigger("focus"); return; } var newAlgorithm = $("#optAddEditRecordDataDsAlgorithm").val(); if ((newAlgorithm === null) || (newAlgorithm === "")) { showAlert("warning", "Missing!", "Please select an DNSSEC algorithm to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataDsAlgorithm").trigger("focus"); return; } var newDigestType = $("#optAddEditRecordDataDsDigestType").val(); if ((newDigestType === null) || (newDigestType === "")) { showAlert("warning", "Missing!", "Please select a Digest Type to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataDsDigestType").trigger("focus"); return; } var digest = divData.attr("data-record-digest"); var newDigest = $("#txtAddEditRecordDataDsDigest").val(); if (newDigest === "") { showAlert("warning", "Missing!", "Please enter the Digest hash in hex string format to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataDsDigest").trigger("focus"); return; } apiUrl += "&keyTag=" + keyTag + "&algorithm=" + algorithm + "&digestType=" + digestType + "&newKeyTag=" + newKeyTag + "&newAlgorithm=" + newAlgorithm + "&newDigestType=" + newDigestType + "&digest=" + encodeURIComponent(digest) + "&newDigest=" + encodeURIComponent(newDigest); break; case "SSHFP": var sshfpAlgorithm = divData.attr("data-record-algorithm"); var sshfpFingerprintType = divData.attr("data-record-fingerprint-type"); var sshfpFingerprint = divData.attr("data-record-fingerprint"); var newSshfpAlgorithm = $("#optAddEditRecordDataSshfpAlgorithm").val(); if ((newSshfpAlgorithm === null) || (newSshfpAlgorithm === "")) { showAlert("warning", "Missing!", "Please select an Algorithm to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataSshfpAlgorithm").trigger("focus"); return; } var newSshfpFingerprintType = $("#optAddEditRecordDataSshfpFingerprintType").val(); if ((newSshfpFingerprintType === null) || (newSshfpFingerprintType === "")) { showAlert("warning", "Missing!", "Please select a Fingerprint Type to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataSshfpFingerprintType").trigger("focus"); return; } var newSshfpFingerprint = $("#txtAddEditRecordDataSshfpFingerprint").val(); if (newSshfpFingerprint === "") { showAlert("warning", "Missing!", "Please enter the Fingerprint hash in hex string format to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataSshfpFingerprint").trigger("focus"); return; } apiUrl += "&sshfpAlgorithm=" + sshfpAlgorithm + "&newSshfpAlgorithm=" + newSshfpAlgorithm + "&sshfpFingerprintType=" + sshfpFingerprintType + "&newSshfpFingerprintType=" + newSshfpFingerprintType + "&sshfpFingerprint=" + encodeURIComponent(sshfpFingerprint) + "&newSshfpFingerprint=" + encodeURIComponent(newSshfpFingerprint); break; case "TLSA": var tlsaCertificateUsage = divData.attr("data-record-certificate-usage"); var tlsaSelector = divData.attr("data-record-selector"); var tlsaMatchingType = divData.attr("data-record-matching-type"); var tlsaCertificateAssociationData = divData.attr("data-record-certificate-association-data"); var newTlsaCertificateUsage = $("#optAddEditRecordDataTlsaCertificateUsage").val(); if ((newTlsaCertificateUsage === null) || (newTlsaCertificateUsage === "")) { showAlert("warning", "Missing!", "Please select a Certificate Usage to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataTlsaCertificateUsage").trigger("focus"); return; } var newTlsaSelector = $("#optAddEditRecordDataTlsaSelector").val(); if ((newTlsaSelector === null) || (newTlsaSelector === "")) { showAlert("warning", "Missing!", "Please select a Selector to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataTlsaSelector").trigger("focus"); return; } var newTlsaMatchingType = $("#optAddEditRecordDataTlsaMatchingType").val(); if ((newTlsaMatchingType === null) || (newTlsaMatchingType === "")) { showAlert("warning", "Missing!", "Please select a Matching Type to update the record.", divAddEditRecordAlert); $("#optAddEditRecordDataTlsaMatchingType").trigger("focus"); return; } var newTlsaCertificateAssociationData = $("#txtAddEditRecordDataTlsaCertificateAssociationData").val(); if (newTlsaCertificateAssociationData === "") { showAlert("warning", "Missing!", "Please enter the Certificate Association Data to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataTlsaCertificateAssociationData").trigger("focus"); return; } apiUrl += "&tlsaCertificateUsage=" + tlsaCertificateUsage + "&newTlsaCertificateUsage=" + newTlsaCertificateUsage + "&tlsaSelector=" + tlsaSelector + "&newTlsaSelector=" + newTlsaSelector + "&tlsaMatchingType=" + tlsaMatchingType + "&newTlsaMatchingType=" + newTlsaMatchingType + "&tlsaCertificateAssociationData=" + encodeURIComponent(tlsaCertificateAssociationData) + "&newTlsaCertificateAssociationData=" + encodeURIComponent(newTlsaCertificateAssociationData); break; case "SVCB": case "HTTPS": var svcPriority = divData.attr("data-record-svc-priority"); var svcTargetName = divData.attr("data-record-svc-target-name"); var svcParams = ""; { var jsonSvcParams = JSON.parse(divData.attr("data-record-svc-params")); for (var paramKey in jsonSvcParams) { if (svcParams.length === 0) svcParams = paramKey + "|" + jsonSvcParams[paramKey]; else svcParams += "|" + paramKey + "|" + jsonSvcParams[paramKey]; } if (svcParams.length === 0) svcParams = false; } var newSvcPriority = $("#txtAddEditRecordDataSvcbPriority").val(); if ((newSvcPriority === null) || (newSvcPriority === "")) { showAlert("warning", "Missing!", "Please enter a Priority value to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataSvcbPriority").trigger("focus"); return; } var newSvcTargetName = $("#txtAddEditRecordDataSvcbTargetName").val(); if ((newSvcTargetName === null) || (newSvcTargetName === "")) { showAlert("warning", "Missing!", "Please enter a Target Name to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataSvcbTargetName").trigger("focus"); return; } var newSvcParams = serializeTableData($("#tableAddEditRecordDataSvcbParams"), 2, divAddEditRecordAlert); if (newSvcParams === false) return; if (newSvcParams.length === 0) newSvcParams = false; var autoIpv4Hint = $("#chkAddEditRecordDataSvcbAutoIpv4Hint").prop("checked"); var autoIpv6Hint = $("#chkAddEditRecordDataSvcbAutoIpv6Hint").prop("checked"); apiUrl += "&svcPriority=" + svcPriority + "&newSvcPriority=" + newSvcPriority + "&svcTargetName=" + encodeURIComponent(svcTargetName) + "&newSvcTargetName=" + encodeURIComponent(newSvcTargetName) + "&svcParams=" + encodeURIComponent(svcParams) + "&newSvcParams=" + encodeURIComponent(newSvcParams) + "&autoIpv4Hint=" + autoIpv4Hint + "&autoIpv6Hint=" + autoIpv6Hint; break; case "URI": var uriPriority = divData.attr("data-record-priority"); var newUriPriority = $("#txtAddEditRecordDataUriPriority").val(); if (newUriPriority === "") { showAlert("warning", "Missing!", "Please enter a suitable priority.", divAddEditRecordAlert); $("#txtAddEditRecordDataUriPriority").trigger("focus"); return; } var uriWeight = divData.attr("data-record-weight"); var newUriWeight = $("#txtAddEditRecordDataUriWeight").val(); if (newUriWeight === "") { showAlert("warning", "Missing!", "Please enter a suitable weight.", divAddEditRecordAlert); $("#txtAddEditRecordDataUriWeight").trigger("focus"); return; } var uri = divData.attr("data-record-uri"); var newUri = $("#txtAddEditRecordDataUri").val(); if (newUri === "") { showAlert("warning", "Missing!", "Please enter a suitable value into the URI field.", divAddEditRecordAlert); $("#txtAddEditRecordDataUri").trigger("focus"); return; } apiUrl += "&uriPriority=" + uriPriority + "&newUriPriority=" + newUriPriority + "&uriWeight=" + uriWeight + "&newUriWeight=" + newUriWeight + "&uri=" + encodeURIComponent(uri) + "&newUri=" + encodeURIComponent(newUri); break; case "CAA": var flags = divData.attr("data-record-flags"); var tag = divData.attr("data-record-tag"); var newFlags = $("#txtAddEditRecordDataCaaFlags").val(); if (newFlags === "") newFlags = 0; var newTag = $("#txtAddEditRecordDataCaaTag").val(); if (newTag === "") newTag = "issue"; var value = divData.attr("data-record-value"); var newValue = $("#txtAddEditRecordDataCaaValue").val(); if (newValue === "") { showAlert("warning", "Missing!", "Please enter a suitable value into the authority field.", divAddEditRecordAlert); $("#txtAddEditRecordDataCaaValue").trigger("focus"); return; } apiUrl += "&flags=" + flags + "&tag=" + encodeURIComponent(tag) + "&newFlags=" + newFlags + "&newTag=" + encodeURIComponent(newTag) + "&value=" + encodeURIComponent(value) + "&newValue=" + encodeURIComponent(newValue); break; case "ANAME": var aname = divData.attr("data-record-aname"); var newAName = $("#txtAddEditRecordDataValue").val(); if (newAName === "") { showAlert("warning", "Missing!", "Please enter a suitable value to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&aname=" + encodeURIComponent(aname) + "&newAName=" + encodeURIComponent(newAName); break; case "FWD": var protocol = divData.attr("data-record-protocol"); var newProtocol = $("input[name=rdAddEditRecordDataForwarderProtocol]:checked").val(); var forwarder = divData.attr("data-record-forwarder"); var newForwarder = $("#txtAddEditRecordDataForwarder").val(); if (newForwarder === "") { showAlert("warning", "Missing!", "Please enter a domain name or IP address or URL as a forwarder to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataForwarder").trigger("focus"); return; } var forwarderPriority = $("#txtAddEditRecordDataForwarderPriority").val(); var dnssecValidation = $("#chkAddEditRecordDataForwarderDnssecValidation").prop("checked"); apiUrl += "&protocol=" + protocol + "&newProtocol=" + newProtocol + "&forwarder=" + encodeURIComponent(forwarder) + "&newForwarder=" + encodeURIComponent(newForwarder) + "&forwarderPriority=" + forwarderPriority + "&dnssecValidation=" + dnssecValidation; if (newForwarder !== "this-server") { var proxyType = $("input[name=rdAddEditRecordDataForwarderProxyType]:checked").val(); apiUrl += "&proxyType=" + proxyType; switch (proxyType) { case "Http": case "Socks5": var proxyAddress = $("#txtAddEditRecordDataForwarderProxyAddress").val(); var proxyPort = $("#txtAddEditRecordDataForwarderProxyPort").val(); var proxyUsername = $("#txtAddEditRecordDataForwarderProxyUsername").val(); var proxyPassword = $("#txtAddEditRecordDataForwarderProxyPassword").val(); if ((proxyAddress == null) || (proxyAddress === "")) { showAlert("warning", "Missing!", "Please enter a domain name or IP address for Proxy Server Address to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataForwarderProxyAddress").trigger("focus"); return; } if ((proxyPort == null) || (proxyPort === "")) { showAlert("warning", "Missing!", "Please enter a port number for Proxy Server Port to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataForwarderProxyPort").trigger("focus"); return; } apiUrl += "&proxyAddress=" + encodeURIComponent(proxyAddress) + "&proxyPort=" + proxyPort + "&proxyUsername=" + encodeURIComponent(proxyUsername) + "&proxyPassword=" + encodeURIComponent(proxyPassword); break; } } break; case "APP": apiUrl += "&appName=" + encodeURIComponent(divData.attr("data-record-app-name")) + "&classPath=" + encodeURIComponent(divData.attr("data-record-classpath")) + "&recordData=" + encodeURIComponent($("#txtAddEditRecordDataData").val()); break; default: type = $("#txtAddEditRecordDataUnknownType").val(); var rdata = divData.attr("data-record-rdata"); var newRData = $("#txtAddEditRecordDataValue").val(); if ((newRData === null) || (newRData === "")) { showAlert("warning", "Missing!", "Please enter a hex value as the RDATA to update the record.", divAddEditRecordAlert); $("#txtAddEditRecordDataValue").trigger("focus"); return; } apiUrl += "&rdata=" + encodeURIComponent(rdata) + "&newRData=" + encodeURIComponent(newRData); break; } var node = $("#optZonesClusterNode").val(); apiUrl = "api/zones/records/update?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&type=" + encodeURIComponent(type) + "&domain=" + encodeURIComponent(domain) + "&newDomain=" + encodeURIComponent(newDomain) + "&ttl=" + ttl + "&disable=" + disable + "&comments=" + encodeURIComponent(comments) + "&expiryTtl=" + expiryTtl + apiUrl; btn.button("loading"); HTTPRequest({ url: apiUrl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#modalAddEditRecord").modal("hide"); //update local data editZoneInfo = responseJSON.response.zone; responseJSON.response.updatedRecord.index = recordIndex; //keep record index for update tasks editZoneRecords[recordIndex] = responseJSON.response.updatedRecord; if ((domain.toLowerCase() !== newDomain.toLowerCase()) && ($("#txtEditZoneFilterName").val() != "")) { //domain updated and filters applied editZoneFilteredRecords = null; //to evaluate filters again //show page showEditZonePage(); } else { editZoneFilteredRecords[index] = responseJSON.response.updatedRecord; //show updated record var zoneType; if (responseJSON.response.zone.internal) zoneType = "Internal"; else zoneType = responseJSON.response.zone.type; var tableHtmlRow = getZoneRecordRowHtml(index, zone, zoneType, responseJSON.response.updatedRecord); $("#trZoneRecord" + index).replaceWith(tableHtmlRow); } showAlert("success", "Record Updated!", "Resource record was updated successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { $("#modalAddEditRecord").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divAddEditRecordAlert }); } function updateRecordState(objBtn, disable) { var btn = $(objBtn); var index = Number(btn.attr("data-id")); var divData = $("#data" + index); var zone = $("#titleEditZone").attr("data-zone"); var recordIndex = Number(divData.attr("data-record-index")); var type = divData.attr("data-record-type"); var domain = divData.attr("data-record-name"); var ttl = divData.attr("data-record-ttl"); var comments = divData.attr("data-record-comments"); var expiryTtl = $("#txtAddEditRecordExpiryTtl").val(); if (domain === "") domain = "."; if (disable && !confirm("Are you sure to disable the " + type + " record '" + domain + "'?")) return; var node = $("#optZonesClusterNode").val(); var apiUrl = "api/zones/records/update?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&type=" + encodeURIComponent(type) + "&domain=" + encodeURIComponent(domain) + "&ttl=" + ttl + "&disable=" + disable + "&comments=" + encodeURIComponent(comments) + "&expiryTtl=" + expiryTtl; switch (type) { case "A": case "AAAA": var updateSvcbHints = zoneHasSvcbAutoHint(type == "A", type == "AAAA"); apiUrl += "&ipAddress=" + encodeURIComponent(divData.attr("data-record-ip-address")) + "&updateSvcbHints=" + updateSvcbHints; break; case "NS": apiUrl += "&nameServer=" + encodeURIComponent(divData.attr("data-record-name-server")) + "&glue=" + encodeURIComponent(divData.attr("data-record-glue")); break; case "CNAME": apiUrl += "&cname=" + encodeURIComponent(divData.attr("data-record-cname")); break; case "PTR": apiUrl += "&ptrName=" + encodeURIComponent(divData.attr("data-record-ptr-name")); break; case "MX": apiUrl += "&preference=" + divData.attr("data-record-preference") + "&exchange=" + encodeURIComponent(divData.attr("data-record-exchange")); break; case "TXT": apiUrl += "&text=" + encodeURIComponent(divData.attr("data-record-text")) + "&splitText=" + divData.attr("data-record-split-text"); break; case "RP": apiUrl += "&mailbox=" + encodeURIComponent(divData.attr("data-record-mailbox")) + "&txtDomain=" + encodeURIComponent(divData.attr("data-record-txt-domain")); break; case "SRV": apiUrl += "&priority=" + divData.attr("data-record-priority") + "&weight=" + divData.attr("data-record-weight") + "&port=" + divData.attr("data-record-port") + "&target=" + encodeURIComponent(divData.attr("data-record-target")); break; case "NAPTR": apiUrl += "&naptrOrder=" + divData.attr("data-record-order") + "&naptrPreference=" + divData.attr("data-record-preference") + "&naptrFlags=" + encodeURIComponent(divData.attr("data-record-flags")) + "&naptrServices=" + encodeURIComponent(divData.attr("data-record-services")) + "&naptrRegexp=" + encodeURIComponent(divData.attr("data-record-regexp")) + "&naptrReplacement=" + encodeURIComponent(divData.attr("data-record-replacement")); break; case "DNAME": apiUrl += "&dname=" + encodeURIComponent(divData.attr("data-record-dname")); break; case "DS": apiUrl += "&keyTag=" + divData.attr("data-record-key-tag") + "&algorithm=" + divData.attr("data-record-algorithm") + "&digestType=" + divData.attr("data-record-digest-type") + "&digest=" + encodeURIComponent(divData.attr("data-record-digest")); break; case "SSHFP": apiUrl += "&sshfpAlgorithm=" + divData.attr("data-record-algorithm") + "&sshfpFingerprintType=" + divData.attr("data-record-fingerprint-type") + "&sshfpFingerprint=" + encodeURIComponent(divData.attr("data-record-fingerprint")); break; case "TLSA": apiUrl += "&tlsaCertificateUsage=" + divData.attr("data-record-certificate-usage") + "&tlsaSelector=" + divData.attr("data-record-selector") + "&tlsaMatchingType=" + divData.attr("data-record-matching-type") + "&tlsaCertificateAssociationData=" + encodeURIComponent(divData.attr("data-record-certificate-association-data")); break; case "SVCB": case "HTTPS": var svcPriority = divData.attr("data-record-svc-priority"); var svcTargetName = divData.attr("data-record-svc-target-name"); var svcParams = ""; { var jsonSvcParams = JSON.parse(divData.attr("data-record-svc-params")); for (var paramKey in jsonSvcParams) { if (svcParams.length == 0) svcParams = paramKey + "|" + jsonSvcParams[paramKey]; else svcParams += "|" + paramKey + "|" + jsonSvcParams[paramKey]; } if (svcParams.length === 0) svcParams = false; } var autoIpv4Hint = divData.attr("data-record-auto-ipv4hint"); var autoIpv6Hint = divData.attr("data-record-auto-ipv6hint"); apiUrl += "&svcPriority=" + svcPriority + "&svcTargetName=" + encodeURIComponent(svcTargetName) + "&svcParams=" + encodeURIComponent(svcParams) + "&autoIpv4Hint=" + autoIpv4Hint + "&autoIpv6Hint=" + autoIpv6Hint; break; case "URI": apiUrl += "&uriPriority=" + divData.attr("data-record-priority") + "&uriWeight=" + encodeURIComponent(divData.attr("data-record-weight")) + "&uri=" + encodeURIComponent(divData.attr("data-record-uri")); break; case "CAA": apiUrl += "&flags=" + divData.attr("data-record-flags") + "&tag=" + encodeURIComponent(divData.attr("data-record-tag")) + "&value=" + encodeURIComponent(divData.attr("data-record-value")); break; case "ANAME": apiUrl += "&aname=" + encodeURIComponent(divData.attr("data-record-aname")); break; case "FWD": apiUrl += "&protocol=" + divData.attr("data-record-protocol") + "&forwarder=" + encodeURIComponent(divData.attr("data-record-forwarder")); var proxyType = divData.attr("data-record-proxy-type"); apiUrl += "&forwarderPriority=" + divData.attr("data-record-priority") + "&dnssecValidation=" + divData.attr("data-record-dnssec-validation") + "&proxyType=" + proxyType; switch (proxyType) { case "Http": case "Socks5": apiUrl += "&proxyAddress=" + encodeURIComponent(divData.attr("data-record-proxy-address")) + "&proxyPort=" + divData.attr("data-record-proxy-port") + "&proxyUsername=" + encodeURIComponent(divData.attr("data-record-proxy-username")) + "&proxyPassword=" + encodeURIComponent(divData.attr("data-record-proxy-password")); break; } break; case "APP": apiUrl += "&appName=" + encodeURIComponent(divData.attr("data-record-app-name")) + "&classPath=" + encodeURIComponent(divData.attr("data-record-classpath")) + "&recordData=" + encodeURIComponent(divData.attr("data-record-data")); break; default: apiUrl += "&rdata=" + encodeURIComponent(divData.attr("data-record-rdata")); break; } btn.button("loading"); HTTPRequest({ url: apiUrl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); //update local data editZoneInfo = responseJSON.response.zone; responseJSON.response.updatedRecord.index = recordIndex; //keep record index for update tasks editZoneRecords[recordIndex] = responseJSON.response.updatedRecord; editZoneFilteredRecords[index] = responseJSON.response.updatedRecord; //show updated record var zoneType; if (responseJSON.response.zone.internal) zoneType = "Internal"; else zoneType = responseJSON.response.zone.type; var tableHtmlRow = getZoneRecordRowHtml(index, zone, zoneType, responseJSON.response.updatedRecord); $("#trZoneRecord" + index).replaceWith(tableHtmlRow); if (disable) showAlert("success", "Record Disabled!", "Resource record was disabled successfully."); else showAlert("success", "Record Enabled!", "Resource record was enabled successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { showPageLogin(); } }); } function deleteRecord(objBtn) { var btn = $(objBtn); var index = btn.attr("data-id"); var divData = $("#data" + index); var zone = $("#titleEditZone").attr("data-zone"); var recordIndex = Number(divData.attr("data-record-index")); var domain = divData.attr("data-record-name"); var type = divData.attr("data-record-type"); if (domain === "") domain = "."; if (!confirm("Are you sure to permanently delete the " + type + " record '" + domain + "'?")) return; var node = $("#optZonesClusterNode").val(); var apiUrl = "api/zones/records/delete?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&domain=" + encodeURIComponent(domain) + "&type=" + encodeURIComponent(type); switch (type) { case "A": case "AAAA": var updateSvcbHints = zoneHasSvcbAutoHint(type == "A", type == "AAAA"); apiUrl += "&ipAddress=" + encodeURIComponent(divData.attr("data-record-ip-address")) + "&updateSvcbHints=" + updateSvcbHints; break; case "NS": apiUrl += "&nameServer=" + encodeURIComponent(divData.attr("data-record-name-server")); break; case "PTR": apiUrl += "&ptrName=" + encodeURIComponent(divData.attr("data-record-ptr-name")); break; case "MX": apiUrl += "&preference=" + divData.attr("data-record-preference") + "&exchange=" + encodeURIComponent(divData.attr("data-record-exchange")); break; case "TXT": apiUrl += "&text=" + encodeURIComponent(divData.attr("data-record-text")) + "&splitText=" + divData.attr("data-record-split-text"); break; case "RP": apiUrl += "&mailbox=" + encodeURIComponent(divData.attr("data-record-mailbox")) + "&txtDomain=" + encodeURIComponent(divData.attr("data-record-txt-domain")); break; case "SRV": apiUrl += "&priority=" + divData.attr("data-record-priority") + "&weight=" + divData.attr("data-record-weight") + "&port=" + divData.attr("data-record-port") + "&target=" + encodeURIComponent(divData.attr("data-record-target")); break; case "NAPTR": apiUrl += "&naptrOrder=" + divData.attr("data-record-order") + "&naptrPreference=" + divData.attr("data-record-preference") + "&naptrFlags=" + encodeURIComponent(divData.attr("data-record-flags")) + "&naptrServices=" + encodeURIComponent(divData.attr("data-record-services")) + "&naptrRegexp=" + encodeURIComponent(divData.attr("data-record-regexp")) + "&naptrReplacement=" + encodeURIComponent(divData.attr("data-record-replacement")); break; case "DS": apiUrl += "&keyTag=" + divData.attr("data-record-key-tag") + "&algorithm=" + divData.attr("data-record-algorithm") + "&digestType=" + divData.attr("data-record-digest-type") + "&digest=" + encodeURIComponent(divData.attr("data-record-digest")); break; case "SSHFP": apiUrl += "&sshfpAlgorithm=" + divData.attr("data-record-algorithm") + "&sshfpFingerprintType=" + divData.attr("data-record-fingerprint-type") + "&sshfpFingerprint=" + encodeURIComponent(divData.attr("data-record-fingerprint")); break; case "TLSA": apiUrl += "&tlsaCertificateUsage=" + divData.attr("data-record-certificate-usage") + "&tlsaSelector=" + divData.attr("data-record-selector") + "&tlsaMatchingType=" + divData.attr("data-record-matching-type") + "&tlsaCertificateAssociationData=" + encodeURIComponent(divData.attr("data-record-certificate-association-data")); break; case "SVCB": case "HTTPS": var svcPriority = divData.attr("data-record-svc-priority"); var svcTargetName = divData.attr("data-record-svc-target-name"); var svcParams = ""; { var jsonSvcParams = JSON.parse(divData.attr("data-record-svc-params")); for (var paramKey in jsonSvcParams) { if (svcParams.length == 0) svcParams = paramKey + "|" + jsonSvcParams[paramKey]; else svcParams += "|" + paramKey + "|" + jsonSvcParams[paramKey]; } if (svcParams.length === 0) svcParams = false; } apiUrl += "&svcPriority=" + svcPriority + "&svcTargetName=" + encodeURIComponent(svcTargetName) + "&svcParams=" + encodeURIComponent(svcParams); break; case "URI": apiUrl += "&uriPriority=" + divData.attr("data-record-priority") + "&uriWeight=" + encodeURIComponent(divData.attr("data-record-weight")) + "&uri=" + encodeURIComponent(divData.attr("data-record-uri")); break; case "CAA": apiUrl += "&flags=" + divData.attr("data-record-flags") + "&tag=" + encodeURIComponent(divData.attr("data-record-tag")) + "&value=" + encodeURIComponent(divData.attr("data-record-value")); break; case "ANAME": apiUrl += "&aname=" + encodeURIComponent(divData.attr("data-record-aname")); break; case "FWD": apiUrl += "&protocol=" + divData.attr("data-record-protocol") + "&forwarder=" + encodeURIComponent(divData.attr("data-record-forwarder")); break; default: var rdata = divData.attr("data-record-rdata"); if (rdata != null) apiUrl += "&rdata=" + encodeURIComponent(rdata); } btn.button("loading"); HTTPRequest({ url: apiUrl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { //update local array editZoneRecords.splice(recordIndex, 1); editZoneFilteredRecords = null; //to evaluate filters again //show page showEditZonePage(); showAlert("success", "Record Deleted!", "Resource record was deleted successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { showPageLogin(); } }); } function showSignZoneModal(zoneName) { $("#divDnssecSignZoneAlert").html(""); $("#lblDnssecSignZoneZoneName").text(zoneName === "." ? "" : zoneName); $("#lblDnssecSignZoneZoneName").attr("data-zone", zoneName); $("#rdDnssecSignZoneAlgorithmEcdsa").prop("checked", true); $("#divDnssecSignZoneRsaParameters").hide(); $("#optDnssecSignZoneRsaHashAlgorithm").val("SHA256"); $("#divDnssecSignZoneEcdsaParameters").show(); $("#optDnssecSignZoneEcdsaCurve").val("P256"); $("#divDnssecSignZoneEddsaParameters").hide(); $("#optDnssecSignZoneEddsaCurve").val("ED25519"); $("#rdDnssecSignZoneKskGenerationAutomatic").prop("checked", true) $("#divDnssecSignZoneRsaKskKeySize").hide(); $("#optDnssecSignZoneRsaKskKeySize").val("2048"); $("#divDnssecSignZonePemKskPrivateKey").hide(); $("#txtDnssecSignZonePemKskPrivateKey").val(""); $("#rdDnssecSignZoneZskGenerationAutomatic").prop("checked", true) $("#divDnssecSignZoneRsaZskKeySize").hide(); $("#optDnssecSignZoneRsaZskKeySize").val("1280"); $("#divDnssecSignZonePemZskPrivateKey").hide(); $("#txtDnssecSignZonePemZskPrivateKey").val(""); $("#rdDnssecSignZoneNxProofNSEC").prop("checked", true); $("#divDnssecSignZoneNSEC3Parameters").hide(); $("#txtDnssecSignZoneNSEC3Iterations").val("0"); $("#txtDnssecSignZoneNSEC3SaltLength").val("0"); $("#txtDnssecSignZoneDnsKeyTtl").val("3600"); $("#txtDnssecSignZoneZskAutoRollover").val("30"); $("#modalDnssecSignZone").modal("show"); } function signPrimaryZone() { var divDnssecSignZoneAlert = $("#divDnssecSignZoneAlert"); var zone = $("#lblDnssecSignZoneZoneName").attr("data-zone"); var algorithm = $("input[name=rdDnssecSignZoneAlgorithm]:checked").val(); var pemKskPrivateKey = $("#txtDnssecSignZonePemKskPrivateKey").val(); var pemZskPrivateKey = $("#txtDnssecSignZonePemZskPrivateKey").val(); var dnsKeyTtl = $("#txtDnssecSignZoneDnsKeyTtl").val(); var zskRolloverDays = $("#txtDnssecSignZoneZskAutoRollover").val(); var nxProof = $("input[name=rdDnssecSignZoneNxProof]:checked").val(); var additionalParameters = ""; if (nxProof === "NSEC3") { var iterations = $("#txtDnssecSignZoneNSEC3Iterations").val(); var saltLength = $("#txtDnssecSignZoneNSEC3SaltLength").val(); additionalParameters += "&iterations=" + iterations + "&saltLength=" + saltLength; } switch (algorithm) { case "RSA": var hashAlgorithm = $("#optDnssecSignZoneRsaHashAlgorithm").val(); var kskKeySize = $("#optDnssecSignZoneRsaKskKeySize").val(); var zskKeySize = $("#optDnssecSignZoneRsaZskKeySize").val(); additionalParameters += "&hashAlgorithm=" + hashAlgorithm + "&kskKeySize=" + kskKeySize + "&zskKeySize=" + zskKeySize; break; case "ECDSA": var curve = $("#optDnssecSignZoneEcdsaCurve").val(); additionalParameters += "&curve=" + curve; break; case "EDDSA": var curve = $("#optDnssecSignZoneEddsaCurve").val(); additionalParameters += "&curve=" + curve; break; } var node = $("#optZonesClusterNode").val(); var btn = $("#btnDnssecSignZone"); btn.button("loading"); HTTPRequest({ url: "api/zones/dnssec/sign?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&algorithm=" + algorithm + "&pemKskPrivateKey=" + encodeURIComponent(pemKskPrivateKey) + "&pemZskPrivateKey=" + encodeURIComponent(pemZskPrivateKey) + "&dnsKeyTtl=" + dnsKeyTtl + "&zskRolloverDays=" + zskRolloverDays + "&nxProof=" + nxProof + additionalParameters + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalDnssecSignZone").modal("hide"); $("#txtDnssecSignZonePemKskPrivateKey").val(""); $("#txtDnssecSignZonePemZskPrivateKey").val(""); var zoneHideDnssecRecords = (localStorage.getItem("zoneHideDnssecRecords") == "true"); if (zoneHideDnssecRecords) { $("#titleEditZoneDnssecStatus").removeClass(); $("#titleEditZoneDnssecStatus").addClass("label label-primary"); $("#titleEditZoneDnssecStatus").show(); $("#lnkZoneDnssecSignZone").hide(); $("#lnkZoneDnssecHideRecords").hide(); $("#lnkZoneDnssecShowRecords").show(); $("#lnkZoneDnssecViewDsRecords").show(); $("#lnkZoneDnssecProperties").show(); $("#lnkZoneDnssecUnsignZone").show(); $("#optAddEditRecordTypeDs").show(); $("#optAddEditRecordTypeSshfp").show(); $("#optAddEditRecordTypeTlsa").show(); $("#optAddEditRecordTypeAName").hide(); $("#optAddEditRecordTypeApp").hide(); } else { showEditZone(zone); } showAlert("success", "Zone Signed!", "The primary zone was signed successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecSignZone").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecSignZoneAlert }); } function showUnsignZoneModal(zoneName) { $("#divDnssecUnsignZoneAlert").html(""); $("#lblDnssecUnsignZoneZoneName").text(zoneName === "." ? "" : zoneName); $("#lblDnssecUnsignZoneZoneName").attr("data-zone", zoneName); $("#modalDnssecUnsignZone").modal("show"); } function unsignPrimaryZone() { var divDnssecUnsignZoneAlert = $("#divDnssecUnsignZoneAlert"); var zone = $("#lblDnssecUnsignZoneZoneName").attr("data-zone"); var node = $("#optZonesClusterNode").val(); var btn = $("#btnDnssecUnsignZone"); btn.button("loading"); HTTPRequest({ url: "api/zones/dnssec/unsign?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); $("#modalDnssecUnsignZone").modal("hide"); var zoneHideDnssecRecords = (localStorage.getItem("zoneHideDnssecRecords") == "true"); if (zoneHideDnssecRecords) { $("#titleEditZoneDnssecStatus").hide(); $("#lnkZoneDnssecSignZone").show(); $("#lnkZoneDnssecHideRecords").hide(); $("#lnkZoneDnssecShowRecords").hide(); $("#lnkZoneDnssecViewDsRecords").hide(); $("#lnkZoneDnssecProperties").hide(); $("#lnkZoneDnssecUnsignZone").hide(); $("#optAddEditRecordTypeDs").hide(); $("#optAddEditRecordTypeSshfp").hide(); $("#optAddEditRecordTypeTlsa").hide(); $("#optAddEditRecordTypeAName").show(); $("#optAddEditRecordTypeApp").show(); } else { showEditZone(zone); } showAlert("success", "Zone Unsigned!", "The primary zone was unsigned successfully."); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecUnsignZone").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecUnsignZoneAlert }); } function showViewDsModal(zoneName) { var divDnssecViewDsAlert = $("#divDnssecViewDsAlert"); var divDnssecViewDsLoader = $("#divDnssecViewDsLoader"); var divDnssecViewDs = $("#divDnssecViewDs"); var lblDnssecViewDsZoneName = $("#lblDnssecViewDsZoneName"); divDnssecViewDsAlert.html(""); lblDnssecViewDsZoneName.text(zoneName === "." ? "" : zoneName); divDnssecViewDsLoader.show(); divDnssecViewDs.hide(); var node = $("#optZonesClusterNode").val(); $("#modalDnssecViewDs").modal("show"); HTTPRequest({ url: "api/zones/dnssec/viewDS?token=" + sessionData.token + "&zone=" + encodeURIComponent(zoneName) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var tableHtmlRows = ""; for (var i = 0; i < responseJSON.response.dsRecords.length; i++) { var rowspan = responseJSON.response.dsRecords[i].digests.length + 1; tableHtmlRows += "" + "" + responseJSON.response.dsRecords[i].keyTag + "" + "" + responseJSON.response.dsRecords[i].dnsKeyState; if ((responseJSON.response.dsRecords[i].dnsKeyState === "Active") && responseJSON.response.dsRecords[i].isRetiring) tableHtmlRows += " (retiring)"; if (responseJSON.response.dsRecords[i].dnsKeyStateReadyBy != null) tableHtmlRows += "
    (ready by: " + moment(responseJSON.response.dsRecords[i].dnsKeyStateReadyBy).local().format("YYYY-MM-DD HH:mm") + ")"; tableHtmlRows += "" + responseJSON.response.dsRecords[i].algorithm + " (" + responseJSON.response.dsRecords[i].algorithmNumber + ")"; for (var j = 0; j < responseJSON.response.dsRecords[i].digests.length; j++) { if (j > 0) tableHtmlRows += ""; tableHtmlRows += "" + responseJSON.response.dsRecords[i].digests[j].digestType + " (" + responseJSON.response.dsRecords[i].digests[j].digestTypeNumber + ")" + responseJSON.response.dsRecords[i].digests[j].digest + ""; tableHtmlRows += ""; } tableHtmlRows += "Public Key
    " + responseJSON.response.dsRecords[i].publicKey + ""; } $("#tableDnssecViewDsBody").html(tableHtmlRows); divDnssecViewDsLoader.hide(); divDnssecViewDs.show(); }, error: function () { divDnssecViewDsLoader.hide(); }, invalidToken: function () { $("#modalDnssecViewDs").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecViewDsAlert, objLoaderPlaceholder: divDnssecViewDsLoader }); } function showDnssecPropertiesModal(zoneName) { var divDnssecPropertiesLoader = $("#divDnssecPropertiesLoader"); var divDnssecProperties = $("#divDnssecProperties"); $("#divDnssecPropertiesAlert").html(""); $("#lblDnssecPropertiesZoneName").text(zoneName === "." ? "" : zoneName); $("#lblDnssecPropertiesZoneName").attr("data-zone", zoneName); $("#divDnssecPropertiesAddKey").collapse("hide"); $("#optDnssecPropertiesAddKeyKeyType").val("KeySigningKey"); $("#optDnssecPropertiesAddKeyAlgorithm").val("ECDSA"); $("#divDnssecPropertiesAddKeyRsaParameters").hide(); $("#optDnssecPropertiesAddKeyRsaHashAlgorithm").val("SHA256"); $("#divDnssecPropertiesAddKeyEcdsaParameters").show(); $("#optDnssecPropertiesAddKeyEcdsaCurve").val("P256"); $("#divDnssecPropertiesAddKeyEddsaParameters").hide(); $("#optDnssecPropertiesAddKeyEddsaCurve").val("ED25519"); $("#rdDnssecPropertiesKeyGenerationAutomatic").prop("checked", true); $("#divDnssecPropertiesAddKeyRsaKeySize").hide(); $("#optDnssecPropertiesAddKeyRsaKeySize").val("1024"); $("#divDnssecPropertiesPemPrivateKey").hide(); $("#divDnssecPropertiesAddKeyAutomaticRollover").hide(); $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(0); divDnssecPropertiesLoader.show(); divDnssecProperties.hide(); $("#modalDnssecProperties").modal("show"); refreshDnssecProperties(divDnssecPropertiesLoader); } function refreshDnssecProperties(divDnssecPropertiesLoader) { var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var divDnssecPropertiesNoteReadyBy = $("#divDnssecPropertiesNoteReadyBy"); var divDnssecPropertiesNoteActiveBy = $("#divDnssecPropertiesNoteActiveBy"); var divDnssecPropertiesNoteRetiredRevoked = $("#divDnssecPropertiesNoteRetiredRevoked"); var node = $("#optZonesClusterNode").val(); divDnssecPropertiesNoteReadyBy.hide(); divDnssecPropertiesNoteActiveBy.hide(); divDnssecPropertiesNoteRetiredRevoked.hide(); HTTPRequest({ url: "api/zones/dnssec/properties/get?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { var tableHtmlRows = ""; var foundGeneratedKey = false; for (var i = 0; i < responseJSON.response.dnssecPrivateKeys.length; i++) { var id = Math.floor(Math.random() * 10000); tableHtmlRows += "" + "" + responseJSON.response.dnssecPrivateKeys[i].keyTag + "" + "" + responseJSON.response.dnssecPrivateKeys[i].keyType + "" + "" + responseJSON.response.dnssecPrivateKeys[i].algorithm + " (" + responseJSON.response.dnssecPrivateKeys[i].algorithmNumber + ")" + "" + responseJSON.response.dnssecPrivateKeys[i].state + ((responseJSON.response.dnssecPrivateKeys[i].state === "Active") && responseJSON.response.dnssecPrivateKeys[i].isRetiring ? " (retiring)" : "") + "" + "" + moment(responseJSON.response.dnssecPrivateKeys[i].stateChangedOn).local().format("YYYY-MM-DD HH:mm"); if (responseJSON.response.dnssecPrivateKeys[i].stateReadyBy != null) tableHtmlRows += "
    (ready by: " + moment(responseJSON.response.dnssecPrivateKeys[i].stateReadyBy).local().format("YYYY-MM-DD HH:mm") + ")"; else if (responseJSON.response.dnssecPrivateKeys[i].stateActiveBy != null) tableHtmlRows += "
    (active by: " + moment(responseJSON.response.dnssecPrivateKeys[i].stateActiveBy).local().format("YYYY-MM-DD HH:mm") + ")"; tableHtmlRows += ""; if (responseJSON.response.dnssecPrivateKeys[i].keyType === "ZoneSigningKey") { switch (responseJSON.response.dnssecPrivateKeys[i].state) { case "Generated": case "Published": case "Ready": case "Active": if (responseJSON.response.dnssecPrivateKeys[i].isRetiring) { tableHtmlRows += "-"; } else { tableHtmlRows += "" + ""; } break; default: tableHtmlRows += "-"; break; } } else { tableHtmlRows += "-"; } tableHtmlRows += "" + ""; switch (responseJSON.response.dnssecPrivateKeys[i].state) { case "Generated": tableHtmlRows += "
      "; tableHtmlRows += "
    • Delete
    • "; tableHtmlRows += "
    "; foundGeneratedKey = true; break; case "Ready": case "Active": if (!responseJSON.response.dnssecPrivateKeys[i].isRetiring) { tableHtmlRows += "
      "; tableHtmlRows += "
    • Rollover
    • "; tableHtmlRows += "
    • Retire
    • "; tableHtmlRows += "
    "; } break; } tableHtmlRows += ""; if (responseJSON.response.dnssecPrivateKeys[i].keyType === "KeySigningKey") { switch (responseJSON.response.dnssecPrivateKeys[i].state) { case "Published": divDnssecPropertiesNoteReadyBy.show(); break; case "Ready": divDnssecPropertiesNoteActiveBy.show(); break; } } switch (responseJSON.response.dnssecPrivateKeys[i].state) { case "Retired": case "Revoked": divDnssecPropertiesNoteRetiredRevoked.show(); break; } } $("#tableDnssecPropertiesPrivateKeysBody").html(tableHtmlRows); $("#btnDnssecPropertiesPublishKeys").prop("disabled", !foundGeneratedKey); switch (responseJSON.response.dnssecStatus) { case "SignedWithNSEC": $("#rdDnssecPropertiesNxProofNSEC").prop("checked", true); $("#divDnssecPropertiesNSEC3Parameters").hide(); $("#txtDnssecPropertiesNSEC3Iterations").val(0); $("#txtDnssecPropertiesNSEC3SaltLength").val(0); $("#btnDnssecPropertiesChangeNxProof").attr("data-nx-proof", "NSEC"); break; case "SignedWithNSEC3": $("#rdDnssecPropertiesNxProofNSEC3").prop("checked", true); $("#divDnssecPropertiesNSEC3Parameters").show(); $("#txtDnssecPropertiesNSEC3Iterations").val(responseJSON.response.nsec3Iterations); $("#txtDnssecPropertiesNSEC3SaltLength").val(responseJSON.response.nsec3SaltLength); $("#btnDnssecPropertiesChangeNxProof").attr("data-nx-proof", "NSEC3"); $("#btnDnssecPropertiesChangeNxProof").attr("data-nsec3-iterations", responseJSON.response.nsec3Iterations); $("#btnDnssecPropertiesChangeNxProof").attr("data-nsec3-salt-length", responseJSON.response.nsec3SaltLength); break; } $("#txtDnssecPropertiesDnsKeyTtl").val(responseJSON.response.dnsKeyTtl); if (divDnssecPropertiesLoader != null) divDnssecPropertiesLoader.hide(); $("#divDnssecProperties").show(); }, error: function () { if (divDnssecPropertiesLoader != null) divDnssecPropertiesLoader.hide(); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert, objLoaderPlaceholder: divDnssecPropertiesLoader }); } function updateDnssecPrivateKey(keyTag, objBtn) { var btn = $(objBtn); var id = btn.attr("data-id"); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var rolloverDays = $("#txtDnssecPropertiesPrivateKeyAutomaticRollover" + id).val(); var node = $("#optZonesClusterNode").val(); btn.button("loading"); HTTPRequest({ url: "api/zones/dnssec/properties/updatePrivateKey?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&keyTag=" + keyTag + "&rolloverDays=" + rolloverDays + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); showAlert("success", "Updated!", "The DNSKEY automatic rollover config was updated successfully.", divDnssecPropertiesAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function deleteDnssecPrivateKey(keyTag, id) { if (!confirm("Are you sure to permanently delete the private key (" + keyTag + ")?")) return; var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var node = $("#optZonesClusterNode").val(); var btn = $("#btnDnssecPropertiesDnsKeyRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/dnssec/properties/deletePrivateKey?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&keyTag=" + keyTag + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#trDnssecPropertiesPrivateKey" + id).remove(); showAlert("success", "Private Key Deleted!", "The DNSSEC private key was deleted successfully.", divDnssecPropertiesAlert); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function rolloverDnssecDnsKey(keyTag, id) { if (!confirm("Are you sure you want to rollover the DNS Key (" + keyTag + ")?")) return; var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var node = $("#optZonesClusterNode").val(); var btn = $("#btnDnssecPropertiesDnsKeyRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/dnssec/properties/rolloverDnsKey?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&keyTag=" + keyTag + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshDnssecProperties(); showAlert("success", "Rollover Done!", "The DNS Key was rolled over successfully.", divDnssecPropertiesAlert); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function retireDnssecDnsKey(keyTag, id) { if (!confirm("Are you sure you want to retire the DNS Key (" + keyTag + ")?")) return; var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var node = $("#optZonesClusterNode").val(); var btn = $("#btnDnssecPropertiesDnsKeyRowOption" + id); var originalBtnHtml = btn.html(); btn.prop("disabled", true); btn.html(""); HTTPRequest({ url: "api/zones/dnssec/properties/retireDnsKey?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&keyTag=" + keyTag + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshDnssecProperties(); showAlert("success", "DNS Key Retired!", "The DNS Key was retired successfully.", divDnssecPropertiesAlert); }, error: function () { btn.prop("disabled", false); btn.html(originalBtnHtml); }, invalidToken: function () { $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function publishAllDnssecPrivateKeys(objBtn) { if (!confirm("Are you sure you want to publish all generated DNSSEC private keys?")) return; var btn = $(objBtn); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var node = $("#optZonesClusterNode").val(); btn.button("loading"); HTTPRequest({ url: "api/zones/dnssec/properties/publishAllPrivateKeys?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&node=" + encodeURIComponent(node), success: function (responseJSON) { refreshDnssecProperties(); btn.button("reset"); showAlert("success", "Keys Published!", "All the generated DNSSEC private keys were published successfully.", divDnssecPropertiesAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function addDnssecPrivateKey(objBtn) { var btn = $(objBtn); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var keyType = $("#optDnssecPropertiesAddKeyKeyType").val(); var algorithm = $("#optDnssecPropertiesAddKeyAlgorithm").val(); var pemPrivateKey = $("#txtDnssecPropertiesPemPrivateKey").val(); var rolloverDays = $("#txtDnssecPropertiesAddKeyAutomaticRollover").val(); var additionalParameters = ""; switch (algorithm) { case "RSA": var hashAlgorithm = $("#optDnssecPropertiesAddKeyRsaHashAlgorithm").val(); var keySize = $("#optDnssecPropertiesAddKeyRsaKeySize").val(); additionalParameters = "&hashAlgorithm=" + hashAlgorithm + "&keySize=" + keySize; break; case "ECDSA": var curve = $("#optDnssecPropertiesAddKeyEcdsaCurve").val(); additionalParameters = "&curve=" + curve; break; case "EDDSA": var curve = $("#optDnssecPropertiesAddKeyEddsaCurve").val(); additionalParameters = "&curve=" + curve; break; } var node = $("#optZonesClusterNode").val(); btn.button("loading"); HTTPRequest({ url: "api/zones/dnssec/properties/addPrivateKey?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&keyType=" + keyType + "&algorithm=" + algorithm + "&pemPrivateKey=" + encodeURIComponent(pemPrivateKey) + "&rolloverDays=" + rolloverDays + additionalParameters + "&node=" + encodeURIComponent(node), success: function (responseJSON) { $("#divDnssecPropertiesAddKey").collapse("hide"); $("#txtDnssecPropertiesPemPrivateKey").val(""); refreshDnssecProperties(); btn.button("reset"); showAlert("success", "Key Added!", "The DNSSEC private key was added successfully.", divDnssecPropertiesAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function changeDnssecNxProof(objBtn) { var btn = $(objBtn); var currentNxProof = btn.attr("data-nx-proof"); var currentIterations = btn.attr("data-nsec3-iterations"); var currentSaltLength = btn.attr("data-nsec3-salt-length"); var nxProof = $("input[name=rdDnssecPropertiesNxProof]:checked").val(); var iterations; var saltLength; var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var apiUrl; switch (currentNxProof) { case "NSEC": if (nxProof === "NSEC") { showAlert("success", "Proof Changed!", "The proof of non-existence was changed successfully.", divDnssecPropertiesAlert) return; } else { var iterations = $("#txtDnssecPropertiesNSEC3Iterations").val(); var saltLength = $("#txtDnssecPropertiesNSEC3SaltLength").val(); apiUrl = "api/zones/dnssec/properties/convertToNSEC3?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&iterations=" + iterations + "&saltLength=" + saltLength; } break; case "NSEC3": if (nxProof === "NSEC3") { iterations = $("#txtDnssecPropertiesNSEC3Iterations").val(); saltLength = $("#txtDnssecPropertiesNSEC3SaltLength").val(); if ((currentIterations == iterations) && (currentSaltLength == saltLength)) { showAlert("success", "Proof Changed!", "The proof of non-existence was changed successfully.", divDnssecPropertiesAlert) return; } else { apiUrl = "api/zones/dnssec/properties/updateNSEC3Params?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&iterations=" + iterations + "&saltLength=" + saltLength; } } else { apiUrl = "api/zones/dnssec/properties/convertToNSEC?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone); } break; default: return; } if (!confirm("Are you sure you want to change the proof of non-existence options for the zone?")) return; var node = $("#optZonesClusterNode").val(); btn.button("loading"); HTTPRequest({ url: apiUrl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.attr("data-nx-proof", nxProof); if (iterations != null) btn.attr("data-nsec3-iterations", iterations); if (saltLength != null) btn.attr("data-nsec3-salt-length", saltLength); btn.button("reset"); var zoneHideDnssecRecords = (localStorage.getItem("zoneHideDnssecRecords") == "true"); if (!zoneHideDnssecRecords) showEditZone(zone); showAlert("success", "Proof Changed!", "The proof of non-existence was changed successfully.", divDnssecPropertiesAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } function updateDnssecDnsKeyTtl(objBtn) { var btn = $(objBtn); var divDnssecPropertiesAlert = $("#divDnssecPropertiesAlert"); var zone = $("#lblDnssecPropertiesZoneName").attr("data-zone"); var ttl = $("#txtDnssecPropertiesDnsKeyTtl").val(); var node = $("#optZonesClusterNode").val(); btn.button("loading"); HTTPRequest({ url: "api/zones/dnssec/properties/updateDnsKeyTtl?token=" + sessionData.token + "&zone=" + encodeURIComponent(zone) + "&ttl=" + ttl + "&node=" + encodeURIComponent(node), success: function (responseJSON) { btn.button("reset"); showAlert("success", "TTL Updated!", "The DNSKEY TTL was updated successfully.", divDnssecPropertiesAlert); }, error: function () { btn.button("reset"); }, invalidToken: function () { btn.button("reset"); $("#modalDnssecProperties").modal("hide"); showPageLogin(); }, objAlertPlaceholder: divDnssecPropertiesAlert }); } ================================================ FILE: DnsServerCore/www/json/dnsclient-server-list-builtin.json ================================================ [ { "name": "Recursive Query", "addresses": [ "recursive-resolver" ] }, { "name": "System DNS", "addresses": [ "system-dns" ] }, { "name": "Cloudflare", "addresses": [ "1.1.1.1", "1.0.0.1", "[2606:4700:4700::1111]", "[2606:4700:4700::1001]" ] }, { "name": "Cloudflare TLS", "addresses": [ "cloudflare-dns.com (1.1.1.1:853)", "cloudflare-dns.com (1.0.0.1:853)", "cloudflare-dns.com ([2606:4700:4700::1111]:853)", "cloudflare-dns.com ([2606:4700:4700::1001]:853)" ] }, { "name": "Cloudflare HTTPS", "addresses": [ "https://cloudflare-dns.com/dns-query (1.1.1.1)" ] }, { "name": "Google", "addresses": [ "8.8.8.8", "8.8.4.4", "[2001:4860:4860::8888]", "[2001:4860:4860::8844]" ] }, { "name": "Google TLS", "addresses": [ "dns.google (8.8.8.8:853)", "dns.google (8.8.4.4:853)", "dns.google ([2001:4860:4860::8888]:853)", "dns.google ([2001:4860:4860::8844]:853)" ] }, { "name": "Google HTTPS", "addresses": [ "https://dns.google/dns-query (8.8.8.8)" ] }, { "name": "Quad9 Secure", "addresses": [ "9.9.9.9", "[2620:fe::fe]" ] }, { "name": "Quad9 Secure TLS", "addresses": [ "dns.quad9.net (9.9.9.9:853)", "dns.quad9.net ([2620:fe::fe]:853)" ] }, { "name": "Quad9 Secure HTTPS", "addresses": [ "https://dns.quad9.net/dns-query (9.9.9.9)" ] }, { "name": "OpenDNS", "addresses": [ "208.67.222.222", "208.67.220.220", "[2620:0:ccc::2]", "[2620:0:ccd::2]" ] }, { "name": "OpenDNS TLS", "addresses": [ "dns.opendns.com (208.67.222.222:853)", "dns.opendns.com (208.67.220.220:853)", "dns.opendns.com ([2620:0:ccc::2]:853)", "dns.opendns.com ([2620:0:ccd::2]:853)" ] }, { "name": "OpenDNS HTTPS", "addresses": [ "https://doh.opendns.com/dns-query (208.67.222.222)" ] }, { "name": "AdGuard", "addresses": [ "94.140.14.14", "94.140.15.15", "[2a10:50c0::ad1:ff]", "[2a10:50c0::ad2:ff]" ] }, { "name": "AdGuard TLS", "addresses": [ "dns.adguard-dns.com (94.140.14.14:853)", "dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)" ] }, { "name": "AdGuard HTTPS", "addresses": [ "https://dns.adguard-dns.com/dns-query (94.140.14.14)" ] }, { "name": "AdGuard QUIC", "addresses": [ "dns.adguard-dns.com (94.140.14.14:853)", "dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)" ] }, { "name": "DNS4EU", "addresses": [ "86.54.11.1", "86.54.11.201", "[2a13:1001::86:54:11:1]", "[2a13:1001::86:54:11:201]" ] }, { "name": "DNS4EU TLS", "addresses": [ "protective.joindns4.eu (86.54.11.1:853)", "protective.joindns4.eu ([2a13:1001::86:54:11:1]:853)" ] }, { "name": "DNS4EU HTTPS", "addresses": [ "https://protective.joindns4.eu/dns-query (86.54.11.1)", "https://protective.joindns4.eu/dns-query ([2a13:1001::86:54:11:1])" ] }, { "name": "Level3", "addresses": [ "4.2.2.1", "4.2.2.2" ] }, { "name": "Ultra", "addresses": [ "156.154.70.1", "156.154.71.1" ] }, { "name": "Dyn", "addresses": [ "216.146.35.35", "216.146.36.36" ] }, { "name": null, "addresses": [ "a.root-servers.net", "b.root-servers.net", "c.root-servers.net", "d.root-servers.net", "e.root-servers.net", "f.root-servers.net", "g.root-servers.net", "h.root-servers.net", "i.root-servers.net", "j.root-servers.net", "k.root-servers.net", "l.root-servers.net", "m.root-servers.net" ] } ] ================================================ FILE: DnsServerCore/www/json/quick-block-lists-builtin.json ================================================ [ { "name": "Default", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" ] }, { "name": "Steven Black [adware + malware]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts" ] }, { "name": "Steven Black [adware + malware + gambling]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling/hosts" ] }, { "name": "Steven Black [adware + malware + porn]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts" ] }, { "name": "Steven Black [adware + malware + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + gambling]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + porn]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-porn/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-social/hosts" ] }, { "name": "Steven Black [adware + malware + gambling + porn]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-porn/hosts" ] }, { "name": "Steven Black [adware + malware + gambling + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-social/hosts" ] }, { "name": "Steven Black [adware + malware + porn + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn-social/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + gambling + porn]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + gambling + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-social/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + porn + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-porn-social/hosts" ] }, { "name": "Steven Black [adware + malware + gambling + porn + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling-porn-social/hosts" ] }, { "name": "Steven Black [adware + malware + fakenews + gambling + porn + social]", "urls": [ "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts" ] }, { "name": "OISD Big [Domains (Wildcards)]", "urls": [ "https://big.oisd.nl/domainswild2" ] }, { "name": "OISD NSFW [Domains (Wildcards)]", "urls": [ "https://nsfw.oisd.nl/domainswild2" ] }, { "name": "Hagezi [Multi Light - Basic protection]", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/light-onlydomains.txt" ] }, { "name": "Hagezi [Multi Normal - All-round protection]", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/multi-onlydomains.txt" ] }, { "name": "Hagezi [Multi PRO - Extended protection]", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/pro-onlydomains.txt" ] }, { "name": "Hagezi [Multi PRO++ - Maximum protection]", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/pro.plus-onlydomains.txt" ] }, { "name": "Hagezi [Multi ULTIMATE - Aggressive protection]", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/wildcard/ultimate-onlydomains.txt" ] }, { "name": "Hagezi Newly Registered Domains (NRD) - 7 days", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt" ] }, { "name": "Hagezi Newly Registered Domains (NRD) - 14 days", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt" ] }, { "name": "Hagezi Newly Registered Domains (NRD) - 21 days", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd21-15.txt" ] }, { "name": "Hagezi Newly Registered Domains (NRD) - 28 days", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd21-15.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd28-22.txt" ] }, { "name": "Hagezi Newly Registered Domains (NRD) - 35 days", "urls": [ "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd7.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd14-8.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd21-15.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd28-22.txt", "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/nrd35-29.txt" ] }, { "name": "Shreshta - Newly Registered Domains (past week) Community Feed", "urls": [ "https://shreshtait.com/newly-registered-domains/nrd-1w" ] }, { "name": "Shreshta - Newly Registered Domains (past month) Community Feed", "urls": [ "https://shreshtait.com/newly-registered-domains/nrd-1m" ] } ] ================================================ FILE: DnsServerCore/www/json/quick-forwarders-list-builtin.json ================================================ [ { "name": "Cloudflare (DNS-over-UDP)", "protocol": "UDP", "addresses": [ "1.1.1.1", "1.0.0.1" ] }, { "name": "Cloudflare (DNS-over-UDP IPv6)", "protocol": "UDP", "addresses": [ "[2606:4700:4700::1111]", "[2606:4700:4700::1001]" ] }, { "name": "Cloudflare (DNS-over-TCP)", "protocol": "TCP", "addresses": [ "1.1.1.1", "1.0.0.1" ] }, { "name": "Cloudflare (DNS-over-TCP IPv6)", "protocol": "TCP", "addresses": [ "[2606:4700:4700::1111]", "[2606:4700:4700::1001]" ] }, { "name": "Cloudflare (DNS-over-TLS)", "protocol": "TLS", "addresses": [ "cloudflare-dns.com (1.1.1.1:853)", "cloudflare-dns.com (1.0.0.1:853)" ] }, { "name": "Cloudflare (DNS-over-TLS IPv6)", "protocol": "TLS", "addresses": [ "cloudflare-dns.com ([2606:4700:4700::1111]:853)", "cloudflare-dns.com ([2606:4700:4700::1001]:853)" ] }, { "name": "Cloudflare (DNS-over-HTTPS)", "protocol": "HTTPS", "addresses": [ "https://cloudflare-dns.com/dns-query (1.1.1.1)", "https://cloudflare-dns.com/dns-query (1.0.0.1)" ] }, { "name": "Cloudflare (DNS-over-HTTPS IPv6)", "protocol": "HTTPS", "addresses": [ "https://cloudflare-dns.com/dns-query ([2606:4700:4700::1111])", "https://cloudflare-dns.com/dns-query ([2606:4700:4700::1001])" ] }, { "name": "Cloudflare (DNS-over-TOR!)", "protocol": "TCP", "addresses": [ "dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion" ], "proxyType": "SOCKS5", "proxyAddress": "127.0.0.1", "proxyPort": 9150, "proxyUsername": "", "proxyPassword": "" }, { "name": "Google (DNS-over-UDP)", "protocol": "UDP", "addresses": [ "8.8.8.8", "8.8.4.4" ] }, { "name": "Google (DNS-over-UDP IPv6)", "protocol": "UDP", "addresses": [ "[2001:4860:4860::8888]", "[2001:4860:4860::8844]" ] }, { "name": "Google (DNS-over-TCP)", "protocol": "TCP", "addresses": [ "8.8.8.8", "8.8.4.4" ] }, { "name": "Google (DNS-over-TCP IPv6)", "protocol": "TCP", "addresses": [ "[2001:4860:4860::8888]", "[2001:4860:4860::8844]" ] }, { "name": "Google (DNS-over-TLS)", "protocol": "TLS", "addresses": [ "dns.google (8.8.8.8:853)", "dns.google (8.8.4.4:853)" ] }, { "name": "Google (DNS-over-TLS IPv6)", "protocol": "TLS", "addresses": [ "dns.google ([2001:4860:4860::8888]:853)", "dns.google ([2001:4860:4860::8844]:853)" ] }, { "name": "Google (DNS-over-HTTPS)", "protocol": "HTTPS", "addresses": [ "https://dns.google/dns-query (8.8.8.8)", "https://dns.google/dns-query (8.8.4.4)" ] }, { "name": "Google (DNS-over-HTTPS IPv6)", "protocol": "HTTPS", "addresses": [ "https://dns.google/dns-query ([2001:4860:4860::8888])", "https://dns.google/dns-query ([2001:4860:4860::8844])" ] }, { "name": "Quad9 Secure (DNS-over-UDP)", "protocol": "UDP", "addresses": [ "9.9.9.9", "149.112.112.112" ] }, { "name": "Quad9 Secure (DNS-over-UDP IPv6)", "protocol": "UDP", "addresses": [ "[2620:fe::fe]", "[2620:fe::9]" ] }, { "name": "Quad9 Secure (DNS-over-TCP)", "protocol": "TCP", "addresses": [ "9.9.9.9", "149.112.112.112" ] }, { "name": "Quad9 Secure (DNS-over-TCP IPv6)", "protocol": "TCP", "addresses": [ "[2620:fe::fe]", "[2620:fe::9]" ] }, { "name": "Quad9 Secure (DNS-over-TLS)", "protocol": "TLS", "addresses": [ "dns.quad9.net (9.9.9.9:853)", "dns.quad9.net (149.112.112.112:853)" ] }, { "name": "Quad9 Secure (DNS-over-TLS IPv6)", "protocol": "TLS", "addresses": [ "dns.quad9.net ([2620:fe::fe]:853)", "dns.quad9.net ([2620:fe::9]:853)" ] }, { "name": "Quad9 Secure (DNS-over-HTTPS)", "protocol": "HTTPS", "addresses": [ "https://dns.quad9.net/dns-query (9.9.9.9)", "https://dns.quad9.net/dns-query (149.112.112.112)" ] }, { "name": "Quad9 Secure (DNS-over-HTTPS IPv6)", "protocol": "HTTPS", "addresses": [ "https://dns.quad9.net/dns-query ([2620:fe::fe])", "https://dns.quad9.net/dns-query ([2620:fe::9])" ] }, { "name": "OpenDNS (DNS-over-UDP)", "protocol": "UDP", "addresses": [ "208.67.222.222", "208.67.220.220" ] }, { "name": "OpenDNS (DNS-over-UDP IPv6)", "protocol": "UDP", "addresses": [ "[2620:0:ccc::2]", "[2620:0:ccd::2]" ] }, { "name": "OpenDNS (DNS-over-TCP)", "protocol": "TCP", "addresses": [ "208.67.222.222", "208.67.220.220" ] }, { "name": "OpenDNS (DNS-over-TCP IPv6)", "protocol": "TCP", "addresses": [ "[2620:0:ccc::2]", "[2620:0:ccd::2]" ] }, { "name": "OpenDNS (DNS-over-TLS)", "protocol": "TLS", "addresses": [ "dns.opendns.com (208.67.222.222:853)", "dns.opendns.com (208.67.220.220:853)" ] }, { "name": "OpenDNS (DNS-over-TLS IPv6)", "protocol": "TLS", "addresses": [ "dns.opendns.com ([2620:0:ccc::2]:853)", "dns.opendns.com ([2620:0:ccd::2]:853)" ] }, { "name": "OpenDNS (DNS-over-HTTPS)", "protocol": "HTTPS", "addresses": [ "https://doh.opendns.com/dns-query (208.67.222.222)", "https://doh.opendns.com/dns-query (208.67.220.220)" ] }, { "name": "OpenDNS (DNS-over-HTTPS IPv6)", "protocol": "HTTPS", "addresses": [ "https://doh.opendns.com/dns-query ([2620:0:ccc::2])", "https://doh.opendns.com/dns-query ([2620:0:ccd::2])" ] }, { "name": "AdGuard (DNS-over-UDP)", "protocol": "UDP", "addresses": [ "94.140.14.14", "94.140.15.15" ] }, { "name": "AdGuard (DNS-over-UDP IPv6)", "protocol": "UDP", "addresses": [ "[2a10:50c0::ad1:ff]", "[2a10:50c0::ad2:ff]" ] }, { "name": "AdGuard (DNS-over-TCP)", "protocol": "TCP", "addresses": [ "94.140.14.14", "94.140.15.15" ] }, { "name": "AdGuard (DNS-over-TCP IPv6)", "protocol": "TCP", "addresses": [ "[2a10:50c0::ad1:ff]", "[2a10:50c0::ad2:ff]" ] }, { "name": "AdGuard (DNS-over-TLS)", "protocol": "TLS", "addresses": [ "dns.adguard-dns.com (94.140.14.14:853)", "dns.adguard-dns.com (94.140.15.15:853)" ] }, { "name": "AdGuard (DNS-over-TLS IPv6)", "protocol": "TLS", "addresses": [ "dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)", "dns.adguard-dns.com ([2a10:50c0::ad2:ff]:853)" ] }, { "name": "AdGuard (DNS-over-HTTPS)", "protocol": "HTTPS", "addresses": [ "https://dns.adguard-dns.com/dns-query (94.140.14.14)", "https://dns.adguard-dns.com/dns-query (94.140.15.15)" ] }, { "name": "AdGuard (DNS-over-HTTPS IPv6)", "protocol": "HTTPS", "addresses": [ "https://dns.adguard-dns.com/dns-query ([2a10:50c0::ad1:ff])", "https://dns.adguard-dns.com/dns-query ([2a10:50c0::ad2:ff])" ] }, { "name": "AdGuard (DNS-over-QUIC)", "protocol": "QUIC", "addresses": [ "dns.adguard-dns.com (94.140.14.14:853)", "dns.adguard-dns.com (94.140.15.15:853)" ] }, { "name": "AdGuard (DNS-over-QUIC IPv6)", "protocol": "QUIC", "addresses": [ "dns.adguard-dns.com ([2a10:50c0::ad1:ff]:853)", "dns.adguard-dns.com ([2a10:50c0::ad2:ff]:853)" ] }, { "name": "DNS4EU (DNS-over-UDP)", "protocol": "UDP", "addresses": [ "86.54.11.1", "86.54.11.201" ] }, { "name": "DNS4EU (DNS-over-UDP IPv6)", "protocol": "UDP", "addresses": [ "[2a13:1001::86:54:11:1]", "[2a13:1001::86:54:11:201]" ] }, { "name": "DNS4EU (DNS-over-TCP)", "protocol": "TCP", "addresses": [ "86.54.11.1", "86.54.11.201" ] }, { "name": "DNS4EU (DNS-over-TCP IPv6)", "protocol": "TCP", "addresses": [ "[2a13:1001::86:54:11:1]", "[2a13:1001::86:54:11:201]" ] }, { "name": "DNS4EU (DNS-over-TLS)", "protocol": "TLS", "addresses": [ "protective.joindns4.eu (86.54.11.1:853)", "protective.joindns4.eu (86.54.11.201:853)" ] }, { "name": "DNS4EU (DNS-over-TLS IPv6)", "protocol": "TLS", "addresses": [ "protective.joindns4.eu ([2a13:1001::86:54:11:1]:853)", "protective.joindns4.eu ([2a13:1001::86:54:11:201]:853)" ] }, { "name": "DNS4EU (DNS-over-HTTPS)", "protocol": "HTTPS", "addresses": [ "https://protective.joindns4.eu/dns-query (86.54.11.1)", "https://protective.joindns4.eu/dns-query (86.54.11.201)" ] }, { "name": "DNS4EU (DNS-over-HTTPS IPv6)", "protocol": "HTTPS", "addresses": [ "https://protective.joindns4.eu/dns-query ([2a13:1001::86:54:11:1])", "https://protective.joindns4.eu/dns-query ([2a13:1001::86:54:11:201])" ] } ] ================================================ FILE: DnsServerCore/www/json/readme.txt ================================================ READ ME ======= This folder contains JSON formatted files that are used by the web app to fetch various lists. The JSON files that end with "-builtin" are the ones that are shipped as a part of the software package and are expected to be overwritten when you update the software. You can override these built-in lists by creating your own custom lists. To do this, create a new JSON file with the exact same name except, replace "-builtin" with "-custom" in the name. Use the same JSON format as the built-in list in your custom list to add items. When a custom list is available, the web app will always prefer it. For example, if you wish to have a custom list of servers listed for DNS Client, copy the "dnsclient-server-list-builtin.json" file as "dnsclient-server-list-custom.json" and edit it to have the desired list of servers. Note! Once the custom list file is saved, you will need to refresh the web app so that it loads the updated custom list. Warning! Editing the built-in json files will make it look like it works well, but when the software is updated, the built-in json file will be overwritten causing you to lose any custom changes that you made. ================================================ FILE: DnsServerCore/www/robots.txt ================================================ User-agent: * Disallow: / ================================================ FILE: DnsServerCore.ApplicationCommon/DnsServerCore.ApplicationCommon.csproj ================================================  net9.0 false Shreyas Zare Technitium Technitium DNS Server https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer DnsServerCore.ApplicationCommon 9.0 false ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsAppRecordRequestHandler.cs ================================================ /* Technitium DNS Server Copyright (C) 2023 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 System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.ApplicationCommon { /// /// Allows a DNS App to handle incoming DNS requests for configured APP records in the DNS server zones. /// public interface IDnsAppRecordRequestHandler { /// /// Allows a DNS App to respond to the incoming DNS requests for an APP record in a primary or secondary zone. /// /// The incoming DNS request to be processed. /// The end point (IP address and port) of the client making the request. /// The protocol using which the request was received. /// Tells if the DNS server is configured to allow recursion for the client making this request. /// The name of the application zone that the APP record belongs to. /// The domain name of the APP record. /// The TTL value set in the APP record. /// The record data in the APP record as required for processing the request. /// The DNS response for the DNS request or null to send NODATA response when QNAME matches APP record name or else NXDOMAIN response with an SOA authority. Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData); /// /// A template of the record data format that is required by this app. This template is populated in the UI to allow the user to edit in the expected values. The format could be JSON or any other custom text based format which the app is programmed to parse. This property is optional and can return null if no APP record data is required by the app. /// string ApplicationRecordDataTemplate { get; } } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsApplication.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 System; using System.Threading.Tasks; namespace DnsServerCore.ApplicationCommon { /// /// Allows an application to initialize itself using the DNS app config. /// public interface IDnsApplication : IDisposable { /// /// Allows initializing the DNS application with a config. This function is also called when the config is updated to allow reloading. /// /// The DNS server interface object that allows access to DNS server properties. /// The DNS application config stored in the dnsApp.config file. Task InitializeAsync(IDnsServer dnsServer, string config); /// /// The description about this app to be shown in the Apps section of the DNS web console. /// string Description { get; } } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsApplicationPreference.cs ================================================ /* 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 . */ namespace DnsServerCore.ApplicationCommon { /// /// Allows an application to specify a preference value to allow the DNS server to sort all installed apps in user preferred order to be used. If an application does not implement this interface then the default preference of 100 is assumed by the DNS server. /// public interface IDnsApplicationPreference { /// /// Returns the preference value configured for the application. /// byte Preference { get; } } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsAuthoritativeRequestHandler.cs ================================================ /* 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 System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.ApplicationCommon { /// /// Lets a DNS App to handle incoming requests for the DNS server authoritatively allowing it to act as an authoritative zone by itself and respond to any requests. /// public interface IDnsAuthoritativeRequestHandler { /// /// Allows a DNS App to respond to an incoming DNS request authoritatively. Response returned may be further processed to resolve CNAME or ANAME records, or referral response. /// /// The incoming DNS request to be processed. /// The end point (IP address and port) of the client making the request. /// The protocol using which the request was received. /// Tells if the DNS server is configured to allow recursion for the client making this request. /// The DNS response for the DNS request or null to let the DNS server core process the request as usual. Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed); } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsPostProcessor.cs ================================================ /* 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 System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.ApplicationCommon { /// /// Lets a DNS App process a response generated by the DNS server allowing it to make changes to the final response that will be sent to the client. /// public interface IDnsPostProcessor { /// /// Allows a DNS App to process the response generated by the DNS server for a given request. This method is called by the DNS Server after a response is generated before being sent to the client. Response returned by this method may be processed by another DNS app performing post processing. /// /// The incoming DNS request received by the DNS server. /// The end point (IP address and port) of the client making the request. /// The protocol using which the request was received. /// The DNS response that was generated by the DNS server for the received DNS request or from another DNS app that performed post processing. /// The DNS response that the DNS server should send to the client or null to drop the DNS request. Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response); } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsQueryLogger.cs ================================================ /* 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 System; using System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.ApplicationCommon { public enum DnsServerResponseType : byte { Authoritative = 1, Recursive = 2, Cached = 3, Blocked = 4, UpstreamBlocked = 5, UpstreamBlockedCached = 6, Dropped = 7 } /// /// Allows a DNS App to log incoming DNS requests and their corresponding responses. /// public interface IDnsQueryLogger { /// /// Allows a DNS App to log incoming DNS requests and responses. This method is called by the DNS Server after an incoming request is processed and a response is sent. /// /// The time stamp of the log entry. /// The incoming DNS request that was received. /// The end point (IP address and port) of the client making the request. /// The protocol using which the request was received. /// The DNS response that was sent. Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response); } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsQueryLogs.cs ================================================ /* 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 System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns.ResourceRecords; namespace DnsServerCore.ApplicationCommon { /// /// Allows the DNS App to be queried using the Query Logs HTTP API call to get a filtered list of DNS query logs recorded by the app. /// public interface IDnsQueryLogs { /// /// Allows DNS Server HTTP API to query the logs recorded by the DNS App. /// /// The page number to be displayed to the user. /// Total entries per page. /// Lists log entries in descending order. /// Optional parameter to filter records by start date time. /// Optional parameter to filter records by end date time. /// Optional parameter to filter records by the client IP address. /// Optional parameter to filter records by the DNS transport protocol. /// Optional parameter to filter records by the type of response. /// Optional parameter to filter records by the response code. /// Optional parameter to filter records by the request QNAME. /// Optional parameter to filter records by the request QTYPE. /// Optional parameter to filter records by the request QCLASS. /// The DnsLogPage object that contains all the entries in the requested page number. Task QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass); } public class DnsLogPage { #region variables readonly long _pageNumber; readonly long _totalPages; readonly long _totalEntries; readonly IReadOnlyList _entries; #endregion #region constructor /// /// Creates a new object initialized with all the log page parameters. /// /// The actual page number of the selected data set. /// The total pages for the selected data set. /// The total number of entries in the selected data set. /// The DNS log entries in this page. public DnsLogPage(long pageNumber, long totalPages, long totalEntries, IReadOnlyList entries) { _pageNumber = pageNumber; _totalPages = totalPages; _totalEntries = totalEntries; _entries = entries; } #endregion #region properties /// /// The actual page number of the selected data set. /// public long PageNumber { get { return _pageNumber; } } /// /// The total pages for the selected data set. /// public long TotalPages { get { return _totalPages; } } /// /// The total number of entries in the selected data set. /// public long TotalEntries { get { return _totalEntries; } } /// /// The DNS log entries in this page. /// public IReadOnlyList Entries { get { return _entries; } } #endregion } public class DnsLogEntry { #region variables readonly long _rowNumber; readonly DateTime _timestamp; readonly IPAddress _clientIpAddress; readonly DnsTransportProtocol _protocol; readonly DnsServerResponseType _responseType; readonly double? _responseRtt; readonly DnsResponseCode _rcode; readonly DnsQuestionRecord _question; readonly string _answer; #endregion #region constructor /// /// Creates a new object initialized with all the log entry parameters. /// /// The row number of the entry in the selected data set. /// The time stamp of the log entry. /// The client IP address of the request. /// The DNS transport protocol of the request. /// The type of response sent by the DNS server. /// The round trip time taken to resolve the request. /// The response code sent by the DNS server. /// The question section in the request. /// The answer in text format sent by the DNS server. public DnsLogEntry(long rowNumber, DateTime timestamp, IPAddress clientIpAddress, DnsTransportProtocol protocol, DnsServerResponseType responseType, double? responseRtt, DnsResponseCode rcode, DnsQuestionRecord question, string answer) { _rowNumber = rowNumber; _timestamp = timestamp; _clientIpAddress = clientIpAddress; _protocol = protocol; _responseType = responseType; _responseRtt = responseRtt; _rcode = rcode; _question = question; _answer = answer; switch (_timestamp.Kind) { case DateTimeKind.Local: _timestamp = _timestamp.ToUniversalTime(); break; case DateTimeKind.Unspecified: _timestamp = DateTime.SpecifyKind(_timestamp, DateTimeKind.Utc); break; } } /// /// Creates a new object initialized with all the log entry parameters. /// /// The row number of the entry in the selected data set. /// The time stamp of the log entry. /// The client IP address of the request. /// The DNS transport protocol of the request. /// The type of response sent by the DNS server. /// The response code sent by the DNS server. /// The question section in the request. /// The answer in text format sent by the DNS server. public DnsLogEntry(long rowNumber, DateTime timestamp, IPAddress clientIpAddress, DnsTransportProtocol protocol, DnsServerResponseType responseType, DnsResponseCode rcode, DnsQuestionRecord question, string answer) { _rowNumber = rowNumber; _timestamp = timestamp; _clientIpAddress = clientIpAddress; _protocol = protocol; _responseType = responseType; _rcode = rcode; _question = question; _answer = answer; switch (_timestamp.Kind) { case DateTimeKind.Local: _timestamp = _timestamp.ToUniversalTime(); break; case DateTimeKind.Unspecified: _timestamp = DateTime.SpecifyKind(_timestamp, DateTimeKind.Utc); break; } } #endregion #region properties /// /// The row number of the entry in the selected data set. /// public long RowNumber { get { return _rowNumber; } } /// /// The time stamp of the log entry. /// public DateTime Timestamp { get { return _timestamp; } } /// /// The client IP address of the request. /// public IPAddress ClientIpAddress { get { return _clientIpAddress; } } /// /// The DNS transport protocol of the request. /// public DnsTransportProtocol Protocol { get { return _protocol; } } /// /// The type of response sent by the DNS server. /// public DnsServerResponseType ResponseType { get { return _responseType; } } /// /// The round trip time taken to resolve the request. /// public double? ResponseRtt { get { return _responseRtt; } } /// /// The response code sent by the DNS server. /// public DnsResponseCode RCODE { get { return _rcode; } } /// /// The question section in the request. /// public DnsQuestionRecord Question { get { return _question; } } /// /// The answer in text format sent by the DNS server. /// public string Answer { get { return _answer; } } #endregion } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsRequestBlockingHandler.cs ================================================ /* Technitium DNS Server Copyright (C) 2023 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 System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.ApplicationCommon { /// /// Lets DNS Apps provide DNS level domain name blocking feature. /// public interface IDnsRequestBlockingHandler { /// /// Specifies if the query domain name in the incoming DNS request is allowed to bypass any configured block lists (including for DNS server's built-in blocking feature). /// /// The incoming DNS request to be processed. /// The end point (IP address and port) of the client making the request. /// Returns true if the query domain name in the incoming DNS request is allowed to bypass blocking. Task IsAllowedAsync(DnsDatagram request, IPEndPoint remoteEP); /// /// Specifies if the query domain name in the incoming DNS request is blocked based on the app's own configured block lists. /// /// The incoming DNS request to be processed. /// The end point (IP address and port) of the client making the request. /// The blocked DNS response for the DNS request or null to let the DNS server core process the request as usual. Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP); } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsRequestController.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 System.Net; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; namespace DnsServerCore.ApplicationCommon { public enum DnsRequestControllerAction { /// /// Allow the request to be processed. /// Allow = 0, /// /// Drop the request without any response. /// DropSilently = 1, /// /// Drop the request with a Refused response. /// DropWithRefused = 2 } /// /// Allows a DNS App to inspect and optionally drop incoming DNS requests before they are processed by the DNS Server core. /// public interface IDnsRequestController { /// /// Allows a DNS App to inspect an incoming DNS request and decide whether to allow or drop it. This method is called by the DNS Server before an incoming request is processed. /// /// The incoming DNS request. /// The end point (IP address and port) of the client making the request. /// The protocol using which the request was received. /// The action that must be taken by the DNS server i.e. if the request must be allowed or dropped. Task GetRequestActionAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol); } } ================================================ FILE: DnsServerCore.ApplicationCommon/IDnsServer.cs ================================================ /* 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 System; using System.Net.Mail; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore.ApplicationCommon { /// /// Provides an interface to access the internal DNS Server core. /// public interface IDnsServer : IDnsClient { /// /// Allows querying the DNS server core directly. This call supports recursion even if its not enabled in the DNS server configuration. The request wont be routed to any of the installed DNS Apps except for processing APP records. The request and its response are not counted in any stats or logged. /// /// The question record containing the details to query. /// The timeout value in milliseconds to wait for response. /// The cancellation token to cancel the operation. /// The DNS response for the DNS query. /// When request times out. Task DirectQueryAsync(DnsQuestionRecord question, int timeout = 4000, CancellationToken cancellationToken = default); /// /// Allows querying the DNS server core directly. This call supports recursion even if its not enabled in the DNS server configuration. The request wont be routed to any of the installed DNS Apps except for processing APP records. The request and its response are not counted in any stats or logged. /// /// The DNS request to query. /// The timeout value in milliseconds to wait for response. /// The cancellation token to cancel the operation. /// The DNS response for the DNS query. /// When request times out. Task DirectQueryAsync(DnsDatagram request, int timeout = 4000, CancellationToken cancellationToken = default); /// /// Writes a log entry to the DNS server log file. /// /// The message to log. void WriteLog(string message); /// /// Writes a log entry to the DNS server log file. /// /// The exception to log. void WriteLog(Exception ex); /// /// The name of this installed application. /// string ApplicationName { get; } /// /// The folder where this application is saved on the disk. Can be used to create temp files, read/write files, etc. for this application. /// string ApplicationFolder { get; } /// /// The primary domain name used by this DNS Server to identify itself. /// string ServerDomain { get; } /// /// The default responsible person email address for this DNS Server. /// MailAddress ResponsiblePerson { get; } /// /// The DNS cache object which provides direct access to the DNS server cache. /// IDnsCache DnsCache { get; } /// /// The proxy server setting on the DNS server to be used when required to make any outbound network connection. /// NetProxy Proxy { get; } /// /// Tells if the DNS server prefers using IPv6 as per the settings. /// bool PreferIPv6 { get; } /// /// Returns the UDP payload size configured in the settings. /// public ushort UdpPayloadSize { get; } } } ================================================ FILE: DnsServerCore.HttpApi/DnsServerCore.HttpApi.csproj ================================================  net9.0 false Shreyas Zare Technitium Technitium DNS Server https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer DnsServerCore.HttpApi 3.1 false enable ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll false ================================================ FILE: DnsServerCore.HttpApi/HttpApiClient.cs ================================================ /* 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.HttpApi.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using System; using System.Buffers.Text; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Web; using TechnitiumLibrary; using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Http.Client; using TechnitiumLibrary.Net.Proxy; namespace DnsServerCore.HttpApi { public sealed class HttpApiClient : IDisposable { #region variables readonly static JsonSerializerOptions _serializerOptions; readonly Uri _serverUrl; string? _token; readonly HttpClient _httpClient; bool _loggedIn; #endregion #region constructor static HttpApiClient() { _serializerOptions = new JsonSerializerOptions(); _serializerOptions.PropertyNameCaseInsensitive = true; } public HttpApiClient(string serverUrl, NetProxy? proxy = null, bool preferIPv6 = false, bool ignoreCertificateErrors = false, IDnsClient? dnsClient = null) : this(new Uri(serverUrl), proxy, preferIPv6, ignoreCertificateErrors, dnsClient) { } public HttpApiClient(Uri serverUrl, NetProxy? proxy = null, bool preferIPv6 = false, bool ignoreCertificateErrors = false, IDnsClient? dnsClient = null) { _serverUrl = serverUrl; HttpClientNetworkHandler handler = new HttpClientNetworkHandler(); handler.Proxy = proxy; handler.NetworkType = preferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default; handler.DnsClient = dnsClient; if (ignoreCertificateErrors) { handler.InnerHandler.SslOptions.RemoteCertificateValidationCallback = delegate (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { return true; }; } else { handler.EnableDANE = true; } _httpClient = new HttpClient(handler); _httpClient.BaseAddress = _serverUrl; _httpClient.DefaultRequestHeaders.Add("user-agent", "Technitium DNS Server HTTP API Client"); _httpClient.Timeout = TimeSpan.FromSeconds(30); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (_disposed) return; _httpClient?.Dispose(); _disposed = true; GC.SuppressFinalize(this); } #endregion #region private private static void CheckResponseStatus(JsonElement rootElement) { if (!rootElement.TryGetProperty("status", out JsonElement jsonStatus)) throw new HttpApiClientException("Invalid JSON response was received."); string? status = jsonStatus.GetString()?.ToLowerInvariant(); switch (status) { case "ok": return; case "error": { Exception? innerException = null; if (rootElement.TryGetProperty("innerErrorMessage", out JsonElement jsonInnerErrorMessage)) innerException = new HttpApiClientException(jsonInnerErrorMessage.GetString()!); if (rootElement.TryGetProperty("errorMessage", out JsonElement jsonErrorMessage)) { if (innerException is null) throw new HttpApiClientException(jsonErrorMessage.GetString()!); throw new HttpApiClientException(jsonErrorMessage.GetString()!, innerException); } throw new HttpApiClientException(); } case "invalid-token": { if (rootElement.TryGetProperty("errorMessage", out JsonElement jsonErrorMessage)) throw new InvalidTokenHttpApiClientException(jsonErrorMessage.GetString()!); throw new InvalidTokenHttpApiClientException(); } case "2fa-required": { if (rootElement.TryGetProperty("errorMessage", out JsonElement jsonErrorMessage)) throw new TwoFactorAuthRequiredHttpApiClientException(jsonErrorMessage.GetString()!); throw new TwoFactorAuthRequiredHttpApiClientException(); } default: throw new HttpApiClientException("Unknown status value was received: " + status); } } #endregion #region public public async Task LoginAsync(string username, string password, string? totp = null, bool includeInfo = false, CancellationToken cancellationToken = default) { if (_loggedIn) throw new HttpApiClientException("Already logged in."); HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(_serverUrl, $"api/user/login")); Dictionary parameters = new Dictionary { { "user", username }, { "pass", password }, { "includeInfo", includeInfo.ToString() } }; if (totp is not null) parameters.Add("totp", totp); httpRequest.Content = new FormUrlEncodedContent(parameters); HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); SessionInfo? sessionInfo = rootElement.Deserialize(_serializerOptions); if (sessionInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); _token = sessionInfo.Token; _loggedIn = true; return sessionInfo; } public async Task LogoutAsync(CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exist to logout."); Stream stream = await _httpClient.GetStreamAsync($"api/user/logout?token={_token}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); _token = null; _loggedIn = false; } public void UseApiToken(string token) { if (_loggedIn) throw new HttpApiClientException("Already logged in. Please logout before using a different API token."); _token = token; _loggedIn = true; } public async Task GetDashboardStatsAsync(string actingUsername, DashboardStatsType type = DashboardStatsType.LastHour, bool utcFormat = false, string acceptLanguage = "en-US,en;q=0.5", bool dontTrimQueryTypeData = false, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); string path = $"api/dashboard/stats/get?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&type={type}&utc={utcFormat}&dontTrimQueryTypeData={dontTrimQueryTypeData}"; if (type == DashboardStatsType.Custom) path += $"&start={startDate:O}&end={endDate:O}"; HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(_serverUrl, path)); httpRequest.Headers.Add("Accept-Language", acceptLanguage); HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); DashboardStats? stats = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (stats is null) throw new HttpApiClientException("Invalid JSON response was received."); return stats; } public async Task GetDashboardTopStatsAsync(string actingUsername, DashboardTopStatsType statsType, int limit = 1000, DashboardStatsType type = DashboardStatsType.LastHour, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); string path = $"api/dashboard/stats/getTop?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&type={type}&statsType={statsType}&limit={limit}"; if (type == DashboardStatsType.Custom) path += $"&start={startDate:O}&end={endDate:O}"; HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(_serverUrl, path)); HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); DashboardStats? stats = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (stats is null) throw new HttpApiClientException("Invalid JSON response was received."); return stats; } public async Task SetClusterSettingsAsync(string actingUsername, IReadOnlyDictionary clusterParameters, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); if (clusterParameters.Count == 0) throw new ArgumentException("At least one parameter must be provided.", nameof(clusterParameters)); foreach (KeyValuePair parameter in clusterParameters) { switch (parameter.Key) { case "token": case "node": throw new ArgumentException($"The '{parameter.Key}' is an invalid Settings parameter.", nameof(clusterParameters)); } } HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(_serverUrl, $"api/settings/set?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}")); httpRequest.Content = new FormUrlEncodedContent(clusterParameters); HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); } public async Task ForceUpdateBlockListsAsync(string actingUsername, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/settings/forceUpdateBlockLists?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); } public async Task TemporaryDisableBlockingAsync(string actingUsername, int minutes, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/settings/temporaryDisableBlocking?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&minutes={minutes}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); } public async Task GetClusterStateAsync(bool includeServerIpAddresses = false, bool includeNodeCertificates = false, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/state?token={_token}&includeServerIpAddresses={includeServerIpAddresses}&includeNodeCertificates={includeNodeCertificates}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (clusterInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); return clusterInfo; } public async Task DeleteClusterAsync(bool forceDelete = false, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/delete?token={_token}&forceDelete={forceDelete}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (clusterInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); return clusterInfo; } public async Task JoinClusterAsync(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyCollection secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/join?token={_token}&secondaryNodeId={secondaryNodeId}&secondaryNodeUrl={HttpUtility.UrlEncode(secondaryNodeUrl.OriginalString)}&secondaryNodeIpAddresses={HttpUtility.UrlEncode(secondaryNodeIpAddresses.Join())}&secondaryNodeCertificate={Base64Url.EncodeToString(secondaryNodeCertificate.Export(X509ContentType.Cert))}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (clusterInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); return clusterInfo; } public async Task DeleteSecondaryNodeAsync(int secondaryNodeId, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/deleteSecondary?token={_token}&secondaryNodeId={secondaryNodeId}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (clusterInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); return clusterInfo; } public async Task UpdateSecondaryNodeAsync(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyCollection secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/updateSecondary?token={_token}&secondaryNodeId={secondaryNodeId}&secondaryNodeUrl={HttpUtility.UrlEncode(secondaryNodeUrl.OriginalString)}&secondaryNodeIpAddresses={HttpUtility.UrlEncode(secondaryNodeIpAddresses.Join())}&secondaryNodeCertificate={Base64Url.EncodeToString(secondaryNodeCertificate.Export(X509ContentType.Cert))}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (clusterInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); return clusterInfo; } public async Task<(Stream, DateTime)> TransferConfigFromPrimaryNodeAsync(DateTime ifModifiedSince = default, IReadOnlyCollection? includeZones = null, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, $"api/admin/cluster/primary/transferConfig?token={_token}&includeZones={(includeZones is null ? "" : includeZones.Join(','))}"); httpRequest.Headers.IfModifiedSince = ifModifiedSince; HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); return (httpResponse.Content.ReadAsStream(cancellationToken), httpResponse.Content.Headers.LastModified?.UtcDateTime ?? DateTime.UtcNow); } public async Task LeaveClusterAsync(bool forceLeave = false, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/secondary/leave?token={_token}&forceLeave={forceLeave}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize(_serializerOptions); if (clusterInfo is null) throw new HttpApiClientException("Invalid JSON response was received."); return clusterInfo; } public async Task NotifySecondaryNodeAsync(int primaryNodeId, Uri primaryNodeUrl, IReadOnlyCollection primaryNodeIpAddresses, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/secondary/notify?token={_token}&primaryNodeId={primaryNodeId}&primaryNodeUrl={HttpUtility.UrlEncode(primaryNodeUrl.OriginalString)}&primaryNodeIpAddresses={HttpUtility.UrlEncode(primaryNodeIpAddresses.Join())}", cancellationToken); using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); JsonElement rootElement = jsonDoc.RootElement; CheckResponseStatus(rootElement); } public async Task ProxyRequest(HttpContext context, string actingUsername, CancellationToken cancellationToken = default) { if (!_loggedIn) throw new HttpApiClientException("No active session exists. Please login and try again."); //read input http request and send http response to node HttpRequest inHttpRequest = context.Request; StringBuilder queryString = new StringBuilder(); queryString.Append("?actingUser=").Append(HttpUtility.UrlEncode(actingUsername)); foreach (KeyValuePair query in inHttpRequest.Query) { string key = query.Key; string value = query.Value.ToString(); switch (key) { case "token": //use http client token value = _token!; break; case "node": //skip node name continue; } queryString.Append('&').Append(key).Append('=').Append(HttpUtility.UrlEncode(value)); } HttpRequestMessage httpRequest = new HttpRequestMessage(new HttpMethod(inHttpRequest.Method), new Uri(_serverUrl, inHttpRequest.Path + queryString.ToString())); if (inHttpRequest.HasFormContentType) { if (inHttpRequest.Form.Keys.Count > 0) { Dictionary formParams = new Dictionary(inHttpRequest.Form.Count); foreach (KeyValuePair formParam in inHttpRequest.Form) { string key = formParam.Key; string value = formParam.Value.ToString(); switch (key) { case "token": //use http client token value = _token!; break; case "node": //skip node name continue; } formParams[key] = value; } httpRequest.Content = new FormUrlEncodedContent(formParams); } else if (inHttpRequest.Form.Files.Count > 0) { MultipartFormDataContent formData = new MultipartFormDataContent(); foreach (IFormFile file in inHttpRequest.Form.Files) formData.Add(new StreamContent(file.OpenReadStream()), file.Name, file.FileName); httpRequest.Content = formData; } else { throw new InvalidOperationException(); } } else { httpRequest.Content = new StreamContent(inHttpRequest.Body); } foreach (KeyValuePair inHeader in inHttpRequest.Headers) { if (!httpRequest.Headers.TryAddWithoutValidation(inHeader.Key, inHeader.Value.ToString())) { if (!inHttpRequest.HasFormContentType) { //add content headers only when there is no form data if (!httpRequest.Content.Headers.TryAddWithoutValidation(inHeader.Key, inHeader.Value.ToString())) throw new InvalidOperationException(); } } } HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); //receive http response and write to output http response HttpResponse outHttpResponse = context.Response; foreach (KeyValuePair> header in httpResponse.Headers) { if (header.Key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase) && (httpResponse.Headers.TransferEncodingChunked == true)) continue; //skip chunked header to allow kestrel to do the chunking if (!outHttpResponse.Headers.TryAdd(header.Key, header.Value.Join())) throw new InvalidOperationException(); } foreach (KeyValuePair> header in httpResponse.Content.Headers) { if (header.Key.Equals("content-length", StringComparison.OrdinalIgnoreCase) && (httpResponse.Headers.TransferEncodingChunked == true)) continue; //skip content length when data is chunked if (!outHttpResponse.Headers.TryAdd(header.Key, header.Value.Join())) throw new InvalidOperationException(); } await httpResponse.Content.CopyToAsync(outHttpResponse.Body, cancellationToken); } #endregion #region properties public Uri ServerUrl { get { return _serverUrl; } } #endregion } } ================================================ FILE: DnsServerCore.HttpApi/HttpApiClientException.cs ================================================ /* 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 System; namespace DnsServerCore.HttpApi { public class HttpApiClientException : Exception { #region constructors public HttpApiClientException() : base() { } public HttpApiClientException(string message) : base(message) { } public HttpApiClientException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore.HttpApi/InvalidTokenHttpApiClientException.cs ================================================ /* 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 System; namespace DnsServerCore.HttpApi { public class InvalidTokenHttpApiClientException : HttpApiClientException { #region constructors public InvalidTokenHttpApiClientException() : base() { } public InvalidTokenHttpApiClientException(string message) : base(message) { } public InvalidTokenHttpApiClientException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerCore.HttpApi/Models/ClusterInfo.cs ================================================ /* 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 System; using System.Collections.Generic; namespace DnsServerCore.HttpApi.Models { public class ClusterInfo { public bool ClusterInitialized { get; set; } public string? ClusterDomain { get; set; } public ushort HeartbeatRefreshIntervalSeconds { get; set; } public ushort HeartbeatRetryIntervalSeconds { get; set; } public ushort ConfigRefreshIntervalSeconds { get; set; } public ushort ConfigRetryIntervalSeconds { get; set; } public DateTime? ConfigLastSynced { get; set; } public List? ClusterNodes { get; set; } public class ClusterNodeInfo { public int Id { get; set; } public required string Name { get; set; } public required Uri Url { get; set; } public required string[] IPAddresses { get; set; } public required string Type { get; set; } public required string State { get; set; } public DateTime? UpSince { get; set; } public DateTime? LastSeen { get; set; } } } } ================================================ FILE: DnsServerCore.HttpApi/Models/DashboardStats.cs ================================================ /* 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 System.Collections.Generic; using System.Linq; namespace DnsServerCore.HttpApi.Models { public enum DashboardStatsType { Unknown = 0, LastHour = 1, LastDay = 2, LastWeek = 3, LastMonth = 4, LastYear = 5, Custom = 6 } public enum DashboardTopStatsType { Unknown = 0, TopClients = 1, TopDomains = 2, TopBlockedDomains = 3 } public class DashboardStats { public StatsData? Stats { get; set; } public ChartData? MainChartData { get; set; } public ChartData? QueryResponseChartData { get; set; } public ChartData? QueryTypeChartData { get; set; } public ChartData? ProtocolTypeChartData { get; set; } public TopClientStats[]? TopClients { get; set; } public TopStats[]? TopDomains { get; set; } public TopStats[]? TopBlockedDomains { get; set; } public void Merge(DashboardStats other, int limit) { if ((Stats is not null) && (other.Stats is not null)) Stats.Merge(other.Stats); if ((MainChartData is not null) && (other.MainChartData is not null)) MainChartData = ChartData.Merge(MainChartData, other.MainChartData, false); if ((QueryResponseChartData is not null) && (other.QueryResponseChartData is not null)) QueryResponseChartData = ChartData.Merge(QueryResponseChartData, other.QueryResponseChartData, false); if ((QueryTypeChartData is not null) && (other.QueryTypeChartData is not null)) QueryTypeChartData = ChartData.Merge(QueryTypeChartData, other.QueryTypeChartData, true); if ((ProtocolTypeChartData is not null) && (other.ProtocolTypeChartData is not null)) ProtocolTypeChartData = ChartData.Merge(ProtocolTypeChartData, other.ProtocolTypeChartData, true); if ((TopClients is not null) && (other.TopClients is not null)) TopClients = TopStats.Merge(TopClients, other.TopClients, limit); if ((TopDomains is not null) && (other.TopDomains is not null)) TopDomains = TopStats.Merge(TopDomains, other.TopDomains, limit); if ((TopBlockedDomains is not null) && (other.TopBlockedDomains is not null)) TopBlockedDomains = TopStats.Merge(TopBlockedDomains, other.TopBlockedDomains, limit); } public class StatsData { public long TotalQueries { get; set; } public long TotalNoError { get; set; } public long TotalServerFailure { get; set; } public long TotalNxDomain { get; set; } public long TotalRefused { get; set; } public long TotalAuthoritative { get; set; } public long TotalRecursive { get; set; } public long TotalCached { get; set; } public long TotalBlocked { get; set; } public long TotalDropped { get; set; } public long TotalClients { get; set; } public int Zones { get; set; } public long CachedEntries { get; set; } public int AllowedZones { get; set; } public int BlockedZones { get; set; } public int AllowListZones { get; set; } public int BlockListZones { get; set; } public void Merge(StatsData statsData) { TotalQueries += statsData.TotalQueries; TotalNoError += statsData.TotalNoError; TotalServerFailure += statsData.TotalServerFailure; TotalNxDomain += statsData.TotalNxDomain; TotalRefused += statsData.TotalRefused; TotalAuthoritative += statsData.TotalAuthoritative; TotalRecursive += statsData.TotalRecursive; TotalCached += statsData.TotalCached; TotalBlocked += statsData.TotalBlocked; TotalDropped += statsData.TotalDropped; if (statsData.TotalClients > TotalClients) TotalClients = statsData.TotalClients; if (statsData.Zones > Zones) Zones = statsData.Zones; if (statsData.CachedEntries > CachedEntries) CachedEntries = statsData.CachedEntries; if (statsData.AllowedZones > AllowedZones) AllowedZones = statsData.AllowedZones; if (statsData.BlockedZones > BlockedZones) BlockedZones = statsData.BlockedZones; if (statsData.AllowListZones > AllowListZones) AllowListZones = statsData.AllowListZones; if (statsData.BlockListZones > BlockListZones) BlockListZones = statsData.BlockListZones; } } public class ChartData { public required string[] Labels { get; set; } public required DataSet[] DataSets { get; set; } internal static ChartData Merge(ChartData x, ChartData y, bool sortByData) { Dictionary> aggregateDataSet = new Dictionary>(x.Labels.Length + y.Labels.Length); foreach (DataSet dataSet in x.DataSets) { Dictionary data = new Dictionary(dataSet.Data.Length); for (int i = 0; i < dataSet.Data.Length; i++) data[x.Labels[i]] = dataSet.Data[i]; aggregateDataSet[dataSet.Label ?? ""] = data; } foreach (DataSet dataSet in y.DataSets) { if (!aggregateDataSet.TryGetValue(dataSet.Label ?? "", out Dictionary? data)) { data = new Dictionary(dataSet.Data.Length); aggregateDataSet[dataSet.Label ?? ""] = data; } for (int i = 0; i < dataSet.Data.Length; i++) { string label = y.Labels[i]; if (data.TryGetValue(label, out long value)) data[label] = value + dataSet.Data[i]; else data[label] = dataSet.Data[i]; } } if (sortByData && (aggregateDataSet.Count == 1)) { //prepare single dataset with sorted data KeyValuePair> firstDataSet = aggregateDataSet.First(); Dictionary dataSet = firstDataSet.Value; List> sortedData = [.. dataSet]; sortedData.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.CompareTo(item1.Value); }); string[] labels = new string[sortedData.Count]; long[] data = new long[sortedData.Count]; for (int i = 0; i < sortedData.Count; i++) { labels[i] = sortedData[i].Key; data[i] = sortedData[i].Value; } return new ChartData { Labels = labels, DataSets = [ new DataSet { Label = firstDataSet.Key == "" ? null : aggregateDataSet.First().Key, Data = data } ] }; } else { //prepare merged labels List mergedLabels = new List(x.Labels.Length + y.Labels.Length); mergedLabels.AddRange(x.Labels); foreach (string label in y.Labels) { if (!mergedLabels.Contains(label)) mergedLabels.Add(label); } //prepare merged datasets with ordered data List mergedDataSets = new List(aggregateDataSet.Count); foreach (KeyValuePair> dataSetEntry in aggregateDataSet) { long[] data = new long[mergedLabels.Count]; for (int i = 0; i < mergedLabels.Count; i++) { string label = mergedLabels[i]; if (dataSetEntry.Value.TryGetValue(label, out long value)) data[i] = value; } mergedDataSets.Add(new DataSet { Label = dataSetEntry.Key == "" ? null : dataSetEntry.Key, Data = data }); } return new ChartData { Labels = [.. mergedLabels], DataSets = [.. mergedDataSets] }; } } public void Trim(int limit) { if (Labels.Length > limit) { string[] newLabels = new string[limit]; for (int i = 0; i < limit - 1; i++) newLabels[i] = Labels[i]; newLabels[limit - 1] = "Others"; Labels = newLabels; foreach (DataSet dataSet in DataSets) dataSet.Trim(limit); } } } public class DataSet { public string? Label { get; set; } public required long[] Data { get; set; } public void Trim(int limit) { if (Data.Length > limit) { long[] newData = new long[limit]; for (int i = 0; i < newData.Length - 1; i++) newData[i] = Data[i]; long othersCount = 0; for (int i = limit; i < Data.Length; i++) othersCount += Data[i]; newData[limit - 1] = othersCount; Data = newData; } } } public class TopStats { public required string Name { get; set; } public required long Hits { get; set; } private static List> GetTopList(List> list, int limit) where T : TopStats { list.Sort(delegate (KeyValuePair item1, KeyValuePair item2) { return item2.Value.Hits.CompareTo(item1.Value.Hits); }); if (list.Count > limit) list.RemoveRange(limit, list.Count - limit); return list; } internal static T[] Merge(T[] x, T[] y, int limit) where T : TopStats { Dictionary aggregateData = new Dictionary(x.Length + y.Length); foreach (T item in x) aggregateData[item.Name] = item; foreach (T item in y) { if (aggregateData.TryGetValue(item.Name, out T? entry)) { entry.Hits += item.Hits; if ((entry is TopClientStats topClientEntry) && (item is TopClientStats topClientItem)) { topClientEntry.Domain ??= topClientItem.Domain; topClientEntry.RateLimited |= topClientItem.RateLimited; } } else { aggregateData[item.Name] = item; } } List> topList = GetTopList([.. aggregateData], limit); T[] z = new T[topList.Count]; for (int i = 0; i < topList.Count; i++) z[i] = topList[i].Value; return z; } } public class TopClientStats : TopStats { public string? Domain { get; set; } public bool RateLimited { get; set; } } } } ================================================ FILE: DnsServerCore.HttpApi/Models/SessionInfo.cs ================================================ /* 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 System.Collections.Generic; namespace DnsServerCore.HttpApi.Models { public class SessionInfo { public string? DisplayName { get; set; } public required string Username { get; set; } public bool? TotpEnabled { get; set; } public string? TokenName { get; set; } public required string Token { get; set; } public DetailedInfo? Info { get; set; } public class DetailedInfo { public required string Version { get; set; } public required string UpTimeStamp { get; set; } public required string DnsServerDomain { get; set; } public required int DefaultRecordTtl { get; set; } public required bool UseSoaSerialDateScheme { get; set; } public required bool DnssecValidation { get; set; } public required Dictionary Permissions { get; set; } } public class PermissionInfo { public required bool CanView { get; set; } public required bool CanModify { get; set; } public required bool CanDelete { get; set; } } } } ================================================ FILE: DnsServerCore.HttpApi/TwoFactorAuthRequiredHttpApiClientException.cs ================================================ /* 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 System; namespace DnsServerCore.HttpApi { public class TwoFactorAuthRequiredHttpApiClientException : InvalidTokenHttpApiClientException { #region constructors public TwoFactorAuthRequiredHttpApiClientException() : base() { } public TwoFactorAuthRequiredHttpApiClientException(string message) : base(message) { } public TwoFactorAuthRequiredHttpApiClientException(string message, Exception innerException) : base(message, innerException) { } #endregion } } ================================================ FILE: DnsServerSystemTrayApp/DnsProvider.cs ================================================ /* Technitium DNS Server Copyright (C) 2023 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 System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using TechnitiumLibrary.IO; using TechnitiumLibrary.Net; namespace DnsServerSystemTrayApp { public class DnsProvider : IComparable { #region variables public string Name; public ICollection Addresses; #endregion #region constructor public DnsProvider(string name, ICollection addresses) { this.Name = name; this.Addresses = addresses; } public DnsProvider(BinaryReader bR) { this.Name = bR.ReadShortString(); this.Addresses = new List(); int count = bR.ReadInt32(); for (int i = 0; i < count; i++) this.Addresses.Add(IPAddressExtensions.ReadFrom(bR)); } #endregion #region static public static DnsProvider[] GetDefaultProviders() { return new DnsProvider[] { new DnsProvider("Technitium", new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }), new DnsProvider("Cloudflare", new IPAddress[] { IPAddress.Parse("1.1.1.1"), IPAddress.Parse("1.0.0.1"), IPAddress.Parse("[2606:4700:4700::1111]"), IPAddress.Parse("[2606:4700:4700::1001]") }), new DnsProvider("Google", new IPAddress[] { IPAddress.Parse("8.8.8.8"), IPAddress.Parse("8.8.4.4"), IPAddress.Parse("[2001:4860:4860::8888]"), IPAddress.Parse("[2001:4860:4860::8844]") }), new DnsProvider("Quad9", new IPAddress[] { IPAddress.Parse("9.9.9.9"), IPAddress.Parse("[2620:fe::fe]") }), new DnsProvider("OpenDNS", new IPAddress[] { IPAddress.Parse("208.67.222.222"), IPAddress.Parse("208.67.220.220"), IPAddress.Parse("[2620:0:ccc::2]"), IPAddress.Parse("[2620:0:ccd::2]") }) }; } #endregion #region public public string GetIpv4Addresses() { string ipv4Addresses = null; foreach (IPAddress address in Addresses) { if (address.AddressFamily == AddressFamily.InterNetwork) { if (ipv4Addresses == null) ipv4Addresses = address.ToString(); else ipv4Addresses += ", " + address.ToString(); } } return ipv4Addresses; } public string GetIpv6Addresses() { string ipv6Addresses = null; foreach (IPAddress address in Addresses) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { if (ipv6Addresses == null) ipv6Addresses = address.ToString(); else ipv6Addresses += ", " + address.ToString(); } } return ipv6Addresses; } public override string ToString() { return Name; } public int CompareTo(DnsProvider other) { return this.Name.CompareTo(other.Name); } public void WriteTo(BinaryWriter bW) { bW.WriteShortString(Name); bW.Write(Addresses.Count); foreach (IPAddress address in Addresses) address.WriteTo(bW); } #endregion } } ================================================ FILE: DnsServerSystemTrayApp/DnsServerSystemTrayApp.csproj ================================================  WinExe net9.0-windows7.0 false true true true DnsServerSystemTrayApp DnsServerSystemTrayApp Shreyas Zare logo2.ico 6.1 false Technitium Technitium DNS Server https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.IO.dll ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll True Resources.resx True ResXFileCodeGenerator Resources.Designer.cs ================================================ FILE: DnsServerSystemTrayApp/MainApplicationContext.cs ================================================ /* 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 DnsServerSystemTrayApp.Properties; using Microsoft.Win32; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Management; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.ServiceProcess; using System.Text; using System.Windows.Forms; using TechnitiumLibrary.Net; namespace DnsServerSystemTrayApp { public class MainApplicationContext : ApplicationContext { #region variables const int SERVICE_WAIT_TIMEOUT_SECONDS = 30; private readonly ServiceController _service = new ServiceController("DnsService"); readonly string _configFile; readonly List _dnsProviders = new List(); private NotifyIcon TrayIcon; private ContextMenuStrip TrayIconContextMenu; private ToolStripMenuItem DashboardMenuItem; private ToolStripMenuItem NetworkDnsMenuItem; private ToolStripMenuItem DefaultNetworkDnsMenuItem; private ToolStripMenuItem ManageNetworkDnsMenuItem; private ToolStripMenuItem ServiceMenuItem; private ToolStripMenuItem StartServiceMenuItem; private ToolStripMenuItem RestartServiceMenuItem; private ToolStripMenuItem StopServiceMenuItem; private ToolStripMenuItem FirewallMenuItem; private ToolStripMenuItem AboutMenuItem; private ToolStripMenuItem AutoStartMenuItem; private ToolStripMenuItem ExitMenuItem; #endregion #region constructor public MainApplicationContext(string configFile, string[] args, ref bool exitApp) { _configFile = configFile; LoadConfig(); InitializeComponent(); if (args.Length > 0) { switch (args[0]) { case "--network-dns-default-exit": SetNetworkDnsToDefault(true); exitApp = true; break; case "--network-dns-default": SetNetworkDnsToDefault(); break; case "--network-dns-item": foreach (DnsProvider dnsProvider in _dnsProviders) { if ((args.Length > 1) && dnsProvider.Name.Equals(args[1])) { NetworkDnsMenuSubItem_Click(new ToolStripMenuItem(dnsProvider.Name) { Tag = dnsProvider }, EventArgs.Empty); break; } } break; case "--network-dns-manage": ManageNetworkDnsMenuItem_Click(this, EventArgs.Empty); break; case "--service-start": StartServiceMenuItem_Click(this, EventArgs.Empty); break; case "--service-restart": RestartServiceMenuItem_Click(this, EventArgs.Empty); break; case "--service-stop": StopServiceMenuItem_Click(this, EventArgs.Empty); break; case "--auto-firewall-entry": if (args.Length > 1) SetAutoFirewallEntry(bool.Parse(args[1])); break; case "--first-run": bool usingLoopbackAsDns = false; try { foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; foreach (IPAddress dnsAddress in nic.GetIPProperties().DnsAddresses) { if (IPAddress.IsLoopback(dnsAddress)) { usingLoopbackAsDns = true; break; } } if (usingLoopbackAsDns) break; } } catch { } if (!usingLoopbackAsDns && MessageBox.Show("Do you want to update this computer's network connections to use the locally running Technitium DNS Server?\r\n\r\nNote! It is recommended that you use the locally running Technitium DNS Server unless you explicitly want to keep using your existing network DNS configuration.", "Switch Network DNS? - Technitium DNS Server", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) SetNetworkDns(new DnsProvider("Technitium", new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback })); break; } } } #endregion #region IDisposable protected override void Dispose(bool disposing) { if (disposing) { TrayIcon?.Dispose(); } base.Dispose(disposing); } #endregion #region private private void InitializeComponent() { // // TrayIconContextMenu // TrayIconContextMenu = new ContextMenuStrip(); TrayIconContextMenu.SuspendLayout(); // // TrayIcon // TrayIcon = new NotifyIcon(); TrayIcon.Icon = Resources.logo2; TrayIcon.Visible = true; TrayIcon.MouseUp += TrayIcon_MouseUp; TrayIcon.ContextMenuStrip = TrayIconContextMenu; TrayIcon.Text = Resources.ServiceName; // // DashboardMenuItem // DashboardMenuItem = new ToolStripMenuItem(); DashboardMenuItem.Name = "DashboardMenuItem"; DashboardMenuItem.Text = Resources.DashboardMenuItem; DashboardMenuItem.Click += DashboardMenuItem_Click; // // NetworkDnsMenuItem // NetworkDnsMenuItem = new ToolStripMenuItem(); NetworkDnsMenuItem.Name = "NetworkDnsMenuItem"; NetworkDnsMenuItem.Text = Resources.NetworkDnsMenuItem; DefaultNetworkDnsMenuItem = new ToolStripMenuItem("Default"); DefaultNetworkDnsMenuItem.Click += DefaultNetworkDnsMenuItem_Click; ManageNetworkDnsMenuItem = new ToolStripMenuItem("Manage"); ManageNetworkDnsMenuItem.Click += ManageNetworkDnsMenuItem_Click; // // ServiceMenuItem // ServiceMenuItem = new ToolStripMenuItem(); ServiceMenuItem.Name = "ServiceMenuItem"; ServiceMenuItem.Text = Resources.ServiceMenuItem; StartServiceMenuItem = new ToolStripMenuItem(Resources.ServiceStartMenuItem); StartServiceMenuItem.Click += StartServiceMenuItem_Click; RestartServiceMenuItem = new ToolStripMenuItem(Resources.ServiceRestartMenuItem); RestartServiceMenuItem.Click += RestartServiceMenuItem_Click; StopServiceMenuItem = new ToolStripMenuItem(Resources.ServiceStopMenuItem); StopServiceMenuItem.Click += StopServiceMenuItem_Click; ServiceMenuItem.DropDownItems.AddRange(new ToolStripItem[] { StartServiceMenuItem, RestartServiceMenuItem, StopServiceMenuItem }); // // FirewallMenuItem // FirewallMenuItem = new ToolStripMenuItem(); FirewallMenuItem.Name = "FirewallMenuItem"; FirewallMenuItem.Text = "Auto &Firewall Entry"; FirewallMenuItem.Click += FirewallMenuItem_Click; // // AboutMenuItem // AboutMenuItem = new ToolStripMenuItem(); AboutMenuItem.Name = "AboutMenuItem"; AboutMenuItem.Text = Resources.AboutMenuItem; AboutMenuItem.Click += AboutMenuItem_Click; // // AutoStartMenuItem // AutoStartMenuItem = new ToolStripMenuItem(); AutoStartMenuItem.Name = "AutoStartMenuItem"; AutoStartMenuItem.Text = "&Auto Start Icon"; AutoStartMenuItem.Click += AutoStartMenuItem_Click; // // ExitMenuItem // ExitMenuItem = new ToolStripMenuItem(); ExitMenuItem.Name = "ExitMenuItem"; ExitMenuItem.Text = Resources.ExitMenuItem; ExitMenuItem.Click += ExitMenuItem_Click; TrayIconContextMenu.Items.AddRange(new ToolStripItem[] { DashboardMenuItem, new ToolStripSeparator(), NetworkDnsMenuItem, ServiceMenuItem, FirewallMenuItem, AboutMenuItem, new ToolStripSeparator(), AutoStartMenuItem, ExitMenuItem }); TrayIconContextMenu.ResumeLayout(false); } private void LoadConfig() { try { using (FileStream fS = new FileStream(_configFile, FileMode.Open, FileAccess.Read)) { BinaryReader bR = new BinaryReader(fS); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DT") throw new InvalidDataException("Invalid DNS Server System Tray App config file format."); switch (bR.ReadByte()) { case 1: int count = bR.ReadInt32(); _dnsProviders.Clear(); for (int i = 0; i < count; i++) _dnsProviders.Add(new DnsProvider(bR)); _dnsProviders.Sort(); break; default: throw new NotSupportedException("DNS Server System Tray App config file format is not supported."); } } } catch (FileNotFoundException) { _dnsProviders.Clear(); _dnsProviders.AddRange(DnsProvider.GetDefaultProviders()); _dnsProviders.Sort(); } catch (Exception ex) { MessageBox.Show("Error occurred while loading config file. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void SaveConfig() { try { using (FileStream fS = new FileStream(_configFile, FileMode.Create, FileAccess.Write)) { BinaryWriter bW = new BinaryWriter(fS); bW.Write(Encoding.ASCII.GetBytes("DT")); bW.Write((byte)1); bW.Write(_dnsProviders.Count); foreach (DnsProvider dnsProvider in _dnsProviders) dnsProvider.WriteTo(bW); } } catch (Exception ex) { MessageBox.Show("Error occurred while saving config file. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private static void SetNetworkDns(DnsProvider dnsProvider) { if (!Program.IsAdmin) { Program.RunAsAdmin("--network-dns-item " + dnsProvider.Name); return; } try { foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; IPInterfaceProperties properties = nic.GetIPProperties(); if ((properties.DnsAddresses.Count > 0) && !properties.DnsAddresses[0].IsIPv6SiteLocal) SetNameServer(nic, dnsProvider.Addresses); } MessageBox.Show("The network DNS servers were set to " + dnsProvider.Name + " successfully.", dnsProvider.Name + " Configured - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show("Error occurred while setting " + dnsProvider.Name + " as network DNS server. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private static void SetNameServer(NetworkInterface nic, ICollection dnsAddresses) { SetNameServerIPv4(nic, dnsAddresses); SetNameServerIPv6(nic, dnsAddresses); } private static void SetNameServerIPv4(NetworkInterface nic, ICollection dnsAddresses) { ManagementClass networkAdapterConfig = new ManagementClass("Win32_NetworkAdapterConfiguration"); ManagementObjectCollection instances = networkAdapterConfig.GetInstances(); foreach (ManagementObject obj in instances) { if ((bool)obj["IPEnabled"] && obj["SettingID"].Equals(nic.Id)) { List dnsServers = new List(); foreach (IPAddress dnsAddress in dnsAddresses) { if (dnsAddress.AddressFamily != AddressFamily.InterNetwork) continue; dnsServers.Add(dnsAddress.ToString()); } ManagementBaseObject objParameter = obj.GetMethodParameters("SetDNSServerSearchOrder"); objParameter["DNSServerSearchOrder"] = dnsServers.ToArray(); ManagementBaseObject response = obj.InvokeMethod("SetDNSServerSearchOrder", objParameter, null); uint returnValue = (uint)response.GetPropertyValue("ReturnValue"); switch (returnValue) { case 0: //success case 1: //reboot required break; case 64: throw new Exception("Method not supported on this platform. WMI error code: " + returnValue); case 65: throw new Exception("Unknown failure. WMI error code: " + returnValue); case 70: throw new Exception("Invalid IP address. WMI error code: " + returnValue); case 96: throw new Exception("Unable to notify DNS service. WMI error code: " + returnValue); case 97: throw new Exception("Interface not configurable. WMI error code: " + returnValue); default: throw new Exception("WMI error code: " + returnValue); } break; } } } private static void SetNameServerIPv6(NetworkInterface nic, ICollection dnsAddresses) { //HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\{} string nameServer = null; foreach (IPAddress dnsAddress in dnsAddresses) { if (dnsAddress.AddressFamily != AddressFamily.InterNetworkV6) continue; if (nameServer == null) nameServer = dnsAddress.ToString(); else nameServer += "," + dnsAddress.ToString(); } if (nameServer == null) nameServer = ""; using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\" + nic.Id, true)) { if (key is not null) key.SetValue("NameServer", nameServer, RegistryValueKind.String); } } private static void SetAutoFirewallEntry(bool value) { try { using (RegistryKey key = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Technitium\DNS Server", true)) { if (key is not null) key.SetValue("AutoFirewallEntry", value ? 1 : 0, RegistryValueKind.DWord); } } catch (Exception ex) { MessageBox.Show("Error occurred while setting auto firewall registry entry value. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private static bool AddressExists(ICollection checkAddresses, ICollection addresses) { foreach (IPAddress checkAddress in checkAddresses) { foreach (IPAddress address in addresses) { if (checkAddress.Equals(address)) return true; } } return false; } private void TrayIcon_MouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { #region Network DNS List networkDnsAddresses = new List(); try { foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; networkDnsAddresses.AddRange(nic.GetIPProperties().DnsAddresses); } } catch { } NetworkDnsMenuItem.DropDownItems.Clear(); NetworkDnsMenuItem.DropDownItems.Add(DefaultNetworkDnsMenuItem); NetworkDnsMenuItem.DropDownItems.Add(new ToolStripSeparator()); bool noItemChecked = true; DefaultNetworkDnsMenuItem.Checked = false; foreach (DnsProvider dnsProvider in _dnsProviders) { ToolStripMenuItem item = new ToolStripMenuItem(dnsProvider.Name); item.Tag = dnsProvider; item.Click += NetworkDnsMenuSubItem_Click; if (AddressExists(networkDnsAddresses, dnsProvider.Addresses)) { item.Checked = true; noItemChecked = false; } NetworkDnsMenuItem.DropDownItems.Add(item); } if (noItemChecked) { foreach (IPAddress dnsAddress in networkDnsAddresses) { if (!dnsAddress.IsIPv6SiteLocal) { DefaultNetworkDnsMenuItem.Checked = true; break; } } } if (_dnsProviders.Count > 0) NetworkDnsMenuItem.DropDownItems.Add(new ToolStripSeparator()); NetworkDnsMenuItem.DropDownItems.Add(ManageNetworkDnsMenuItem); #endregion #region service try { _service.Refresh(); switch (_service.Status) { case ServiceControllerStatus.Stopped: DashboardMenuItem.Enabled = false; StartServiceMenuItem.Enabled = true; RestartServiceMenuItem.Enabled = false; StopServiceMenuItem.Enabled = false; break; case ServiceControllerStatus.Running: DashboardMenuItem.Enabled = true; StartServiceMenuItem.Enabled = false; RestartServiceMenuItem.Enabled = true; StopServiceMenuItem.Enabled = true; break; default: DashboardMenuItem.Enabled = false; StartServiceMenuItem.Enabled = false; RestartServiceMenuItem.Enabled = false; StopServiceMenuItem.Enabled = false; break; } ServiceMenuItem.Enabled = true; } catch { DashboardMenuItem.Enabled = false; ServiceMenuItem.Enabled = false; } #endregion #region auto firewall bool autoFirewallEntry = true; try { using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Technitium\DNS Server", false)) { if (key is not null) autoFirewallEntry = Convert.ToInt32(key.GetValue("AutoFirewallEntry", 1)) == 1; } } catch { } FirewallMenuItem.Checked = autoFirewallEntry; #endregion #region auto start try { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", false)) { if (key is not null) { string autoStartPath = key.GetValue("Technitium DNS System Tray") as string; AutoStartMenuItem.Checked = (autoStartPath != null) && autoStartPath.Equals("\"" + Program.APP_PATH + "\""); } } } catch { } #endregion TrayIcon.ShowContextMenu(); } } private void DashboardMenuItem_Click(object sender, EventArgs e) { int port = 5380; string host = "localhost"; //try finding port number from web service config file try { string webServiceConfigFile = Path.Combine(Path.GetDirectoryName(Program.APP_PATH), "config", "webservice.config"); using (FileStream fS = new FileStream(webServiceConfigFile, FileMode.Open, FileAccess.Read)) { BinaryReader bR = new BinaryReader(fS); if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "WC") //format throw new InvalidDataException("DNS Server config file format is invalid."); int version = bR.ReadByte(); if (version > 0) { port = bR.ReadInt32(); //http port _ = bR.ReadInt32(); //https port { int count = bR.ReadByte(); if (count > 0) { IPAddress localAddress = IPAddressExtensions.ReadFrom(bR); if (!IPAddress.IPv6Any.Equals(localAddress) && !IPAddress.Any.Equals(localAddress) && !IPAddress.IsLoopback(localAddress)) host = localAddress.ToString(); } } } } } catch { } ProcessStartInfo processInfo = new ProcessStartInfo($"http://{host}:{port}"); processInfo.UseShellExecute = true; processInfo.Verb = "open"; Process.Start(processInfo); } private void DefaultNetworkDnsMenuItem_Click(object sender, EventArgs e) { SetNetworkDnsToDefault(); } private static void SetNetworkDnsToDefault(bool silent = false) { if (!Program.IsAdmin) { if (!silent) Program.RunAsAdmin("--network-dns-default"); return; } try { foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; SetNameServerIPv6(nic, Array.Empty()); try { IPInterfaceProperties properties = nic.GetIPProperties(); if (properties.GetIPv4Properties().IsDhcpEnabled) { SetNameServerIPv4(nic, Array.Empty()); } else if (properties.GatewayAddresses.Count > 0) { SetNameServerIPv4(nic, new IPAddress[] { properties.GatewayAddresses[0].Address }); } else { SetNameServerIPv4(nic, Array.Empty()); } } catch (NetworkInformationException) { } } if (!silent) MessageBox.Show("The network DNS servers were set to default successfully.", "Default DNS Set - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { if (!silent) MessageBox.Show("Error occurred while setting default network DNS servers. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void ManageNetworkDnsMenuItem_Click(object sender, EventArgs e) { if (!Program.IsAdmin) { Program.RunAsAdmin("--network-dns-manage"); return; } using (frmManageDnsProviders frm = new frmManageDnsProviders(_dnsProviders)) { if (frm.ShowDialog() == DialogResult.OK) { _dnsProviders.Clear(); _dnsProviders.AddRange(frm.DnsProviders); _dnsProviders.Sort(); SaveConfig(); } } } private void NetworkDnsMenuSubItem_Click(object sender, EventArgs e) { ToolStripMenuItem item = sender as ToolStripMenuItem; DnsProvider dnsProvider = item.Tag as DnsProvider; SetNetworkDns(dnsProvider); } private void StartServiceMenuItem_Click(object sender, EventArgs e) { if (!Program.IsAdmin) { Program.RunAsAdmin("--service-start"); return; } try { _service.Start(); _service.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS)); MessageBox.Show("The service was started successfully.", "Service Started - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (System.ServiceProcess.TimeoutException ex) { MessageBox.Show("The service did not respond in time." + ex.Message, "Service Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } catch (Exception ex) { MessageBox.Show("Error occurred while starting service. " + ex.Message, "Service Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void RestartServiceMenuItem_Click(object sender, EventArgs e) { if (!Program.IsAdmin) { Program.RunAsAdmin("--service-restart"); return; } try { _service.Stop(); _service.WaitForStatus(ServiceControllerStatus.Stopped, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS)); _service.Start(); _service.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS)); MessageBox.Show("The service was restarted successfully.", "Service Restarted - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (System.ServiceProcess.TimeoutException ex) { MessageBox.Show("The service did not respond in time." + ex.Message, "Service Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } catch (Exception ex) { MessageBox.Show("Error occurred while restarting service. " + ex.Message, "Service Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void StopServiceMenuItem_Click(object sender, EventArgs e) { if (!Program.IsAdmin) { Program.RunAsAdmin("--service-stop"); return; } try { _service.Stop(); _service.WaitForStatus(ServiceControllerStatus.Stopped, new TimeSpan(0, 0, SERVICE_WAIT_TIMEOUT_SECONDS)); MessageBox.Show("The service was stopped successfully.", "Service Stopped - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (System.ServiceProcess.TimeoutException ex) { MessageBox.Show("The service did not respond in time." + ex.Message, "Service Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } catch (Exception ex) { MessageBox.Show("Error occurred while stopping service. " + ex.Message, "Service Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void FirewallMenuItem_Click(object sender, EventArgs e) { if (!Program.IsAdmin) { Program.RunAsAdmin("--auto-firewall-entry " + (!FirewallMenuItem.Checked).ToString()); return; } SetAutoFirewallEntry(!FirewallMenuItem.Checked); } private void AboutMenuItem_Click(object sender, EventArgs e) { using (frmAbout frm = new frmAbout()) { frm.ShowDialog(); } } private void AutoStartMenuItem_Click(object sender, EventArgs e) { if (AutoStartMenuItem.Checked) { //remove try { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true)) { if (key is not null) key.DeleteValue("Technitium DNS System Tray", false); } } catch (Exception ex) { MessageBox.Show("Error occurred while removing auto start registry entry. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } else { //add try { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true)) { if (key is not null) key.SetValue("Technitium DNS System Tray", "\"" + Program.APP_PATH + "\"", RegistryValueKind.String); } } catch (Exception ex) { MessageBox.Show("Error occurred while adding auto start registry entry. " + ex.Message, "Error - " + Resources.ServiceName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void ExitMenuItem_Click(object sender, EventArgs e) { if (MessageBox.Show(Resources.AreYouSureYouWantToQuit, Resources.Quit + " - " + Resources.ServiceName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes) Application.Exit(); } #endregion } } ================================================ FILE: DnsServerSystemTrayApp/NotifyIconExtension.cs ================================================ /* Technitium DNS Server Copyright (C) 2019 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 System.Reflection; using System.Windows.Forms; namespace DnsServerSystemTrayApp { public static class NotifyIconExtension { public static void ShowContextMenu(this NotifyIcon notifyIcon) { MethodInfo methodInfo = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic); methodInfo.Invoke(notifyIcon, null); } } } ================================================ FILE: DnsServerSystemTrayApp/Program.cs ================================================ /* Technitium DNS Server Copyright (C) 2022 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 System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.Principal; using System.Threading; using System.Windows.Forms; namespace DnsServerSystemTrayApp { static class Program { #region variables const string MUTEX_NAME = "TechnitiumDnsServerSystemTrayApp"; public static readonly string APP_PATH = Assembly.GetEntryAssembly().Location; static readonly bool _isAdmin = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); static Mutex _app; #endregion #region constructor static Program() { if (APP_PATH.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) APP_PATH = APP_PATH.Substring(0, APP_PATH.Length - 4) + ".exe"; } #endregion #region public [STAThread] public static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); #region check for multiple instances _app = new Mutex(true, MUTEX_NAME, out bool createdNewMutex); bool exitApp = false; if (!createdNewMutex) { if (args.Length == 0) { MessageBox.Show("Technitium DNS Server system tray app is already running.", "Already Running!", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } else { exitApp = true; } } #endregion string configFile = Path.Combine(Path.GetDirectoryName(APP_PATH), "SystemTrayApp.config"); MainApplicationContext mainApp = new MainApplicationContext(configFile, args, ref exitApp); if (exitApp) mainApp.Dispose(); else Application.Run(mainApp); } public static void RunAsAdmin(string args) { if (_isAdmin) throw new Exception("App is already running as admin."); ProcessStartInfo processInfo = new ProcessStartInfo(APP_PATH, args); processInfo.UseShellExecute = true; processInfo.Verb = "runas"; try { _app.Dispose(); Process.Start(processInfo); Application.Exit(); return; } catch (Exception ex) { MessageBox.Show("Error! " + ex.Message, "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error); } //user cancels UAC or exception occurred _app = new Mutex(true, MUTEX_NAME, out _); } #endregion #region properties public static bool IsAdmin { get { return _isAdmin; } } #endregion } } ================================================ FILE: DnsServerSystemTrayApp/Properties/PublishProfiles/FolderProfile.pubxml ================================================  Release Any CPU ..\DnsServerWindowsSetup\publish FileSystem <_TargetId>Folder net9.0-windows7.0 false ================================================ FILE: DnsServerSystemTrayApp/Properties/Resources.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace DnsServerSystemTrayApp.Properties { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DnsServerSystemTrayApp.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized string similar to A&bout. /// internal static string AboutMenuItem { get { return ResourceManager.GetString("AboutMenuItem", resourceCulture); } } /// /// Looks up a localized string similar to Are you sure to close the system tray icon? /// ///Closing the system tray icon will not stop Technitium DNS Server.. /// internal static string AreYouSureYouWantToQuit { get { return ResourceManager.GetString("AreYouSureYouWantToQuit", resourceCulture); } } /// /// Looks up a localized string similar to &Dashboard. /// internal static string DashboardMenuItem { get { return ResourceManager.GetString("DashboardMenuItem", resourceCulture); } } /// /// Looks up a localized string similar to E&xit. /// internal static string ExitMenuItem { get { return ResourceManager.GetString("ExitMenuItem", resourceCulture); } } /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// internal static System.Drawing.Bitmap logo { get { object obj = ResourceManager.GetObject("logo", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// internal static System.Drawing.Icon logo2 { get { object obj = ResourceManager.GetObject("logo2", resourceCulture); return ((System.Drawing.Icon)(obj)); } } /// /// Looks up a localized string similar to Network DNS. /// internal static string NetworkDnsMenuItem { get { return ResourceManager.GetString("NetworkDnsMenuItem", resourceCulture); } } /// /// Looks up a localized string similar to Close System Tray Icon?. /// internal static string Quit { get { return ResourceManager.GetString("Quit", resourceCulture); } } /// /// Looks up a localized string similar to &Service. /// internal static string ServiceMenuItem { get { return ResourceManager.GetString("ServiceMenuItem", resourceCulture); } } /// /// Looks up a localized string similar to Technitium DNS Server. /// internal static string ServiceName { get { return ResourceManager.GetString("ServiceName", resourceCulture); } } /// /// Looks up a localized string similar to R&estart. /// internal static string ServiceRestartMenuItem { get { return ResourceManager.GetString("ServiceRestartMenuItem", resourceCulture); } } /// /// Looks up a localized string similar to &Start. /// internal static string ServiceStartMenuItem { get { return ResourceManager.GetString("ServiceStartMenuItem", resourceCulture); } } /// /// Looks up a localized string similar to St&op. /// internal static string ServiceStopMenuItem { get { return ResourceManager.GetString("ServiceStopMenuItem", resourceCulture); } } } } ================================================ FILE: DnsServerSystemTrayApp/Properties/Resources.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 A&bout Are you sure to close the system tray icon? Closing the system tray icon will not stop Technitium DNS Server. &Dashboard E&xit ..\logo.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\logo2.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a Network DNS Close System Tray Icon? &Service Technitium DNS Server R&estart &Start St&op ================================================ FILE: DnsServerSystemTrayApp/frmAbout.Designer.cs ================================================ namespace DnsServerSystemTrayApp { partial class frmAbout { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmAbout)); panel1 = new System.Windows.Forms.Panel(); pictureBox1 = new System.Windows.Forms.PictureBox(); label2 = new System.Windows.Forms.Label(); label4 = new System.Windows.Forms.Label(); lnkTerms = new System.Windows.Forms.LinkLabel(); btnClose = new System.Windows.Forms.Button(); label3 = new System.Windows.Forms.Label(); lnkWebsite = new System.Windows.Forms.LinkLabel(); label1 = new System.Windows.Forms.Label(); lnkContactEmail = new System.Windows.Forms.LinkLabel(); labVersion = new System.Windows.Forms.Label(); label5 = new System.Windows.Forms.Label(); panel1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); SuspendLayout(); // // panel1 // panel1.BackColor = System.Drawing.Color.FromArgb(102, 153, 255); panel1.Controls.Add(pictureBox1); panel1.Dock = System.Windows.Forms.DockStyle.Left; panel1.Location = new System.Drawing.Point(0, 0); panel1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); panel1.Name = "panel1"; panel1.Size = new System.Drawing.Size(58, 301); panel1.TabIndex = 21; // // pictureBox1 // pictureBox1.BackColor = System.Drawing.Color.FromArgb(102, 153, 255); pictureBox1.Dock = System.Windows.Forms.DockStyle.Bottom; pictureBox1.Image = Properties.Resources.logo; pictureBox1.Location = new System.Drawing.Point(0, 243); pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); pictureBox1.Name = "pictureBox1"; pictureBox1.Padding = new System.Windows.Forms.Padding(5); pictureBox1.Size = new System.Drawing.Size(58, 58); pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize; pictureBox1.TabIndex = 12; pictureBox1.TabStop = false; // // label2 // label2.AutoSize = true; label2.Font = new System.Drawing.Font("Arial", 30F, System.Drawing.FontStyle.Bold); label2.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69); label2.Location = new System.Drawing.Point(88, 28); label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label2.Name = "label2"; label2.Size = new System.Drawing.Size(456, 46); label2.TabIndex = 24; label2.Text = "Technitium DNS Server"; // // label4 // label4.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; label4.Font = new System.Drawing.Font("Arial", 8F); label4.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69); label4.Location = new System.Drawing.Point(72, 223); label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label4.Name = "label4"; label4.Size = new System.Drawing.Size(509, 51); label4.TabIndex = 33; label4.Text = resources.GetString("label4.Text"); // // lnkTerms // lnkTerms.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; lnkTerms.AutoSize = true; lnkTerms.Font = new System.Drawing.Font("Arial", 9F); lnkTerms.LinkColor = System.Drawing.Color.FromArgb(102, 153, 255); lnkTerms.Location = new System.Drawing.Point(72, 273); lnkTerms.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); lnkTerms.Name = "lnkTerms"; lnkTerms.Size = new System.Drawing.Size(116, 15); lnkTerms.TabIndex = 32; lnkTerms.TabStop = true; lnkTerms.Text = "Terms && Conditions"; lnkTerms.VisitedLinkColor = System.Drawing.Color.White; lnkTerms.LinkClicked += lnkTerms_LinkClicked; // // btnClose // btnClose.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel; btnClose.Location = new System.Drawing.Point(659, 264); btnClose.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); btnClose.Name = "btnClose"; btnClose.Size = new System.Drawing.Size(88, 27); btnClose.TabIndex = 31; btnClose.Text = "&Close"; btnClose.UseVisualStyleBackColor = true; // // label3 // label3.AutoSize = true; label3.Font = new System.Drawing.Font("Arial", 10F); label3.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69); label3.Location = new System.Drawing.Point(555, 166); label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label3.Name = "label3"; label3.Size = new System.Drawing.Size(58, 16); label3.TabIndex = 37; label3.Text = "Website"; // // lnkWebsite // lnkWebsite.AutoSize = true; lnkWebsite.Font = new System.Drawing.Font("Arial", 10F); lnkWebsite.LinkColor = System.Drawing.Color.FromArgb(102, 153, 255); lnkWebsite.Location = new System.Drawing.Point(555, 185); lnkWebsite.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); lnkWebsite.Name = "lnkWebsite"; lnkWebsite.Size = new System.Drawing.Size(128, 16); lnkWebsite.TabIndex = 36; lnkWebsite.TabStop = true; lnkWebsite.Text = "technitium.com/dns"; lnkWebsite.VisitedLinkColor = System.Drawing.Color.White; lnkWebsite.LinkClicked += lnkWebsite_LinkClicked; // // label1 // label1.AutoSize = true; label1.Font = new System.Drawing.Font("Arial", 10F); label1.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69); label1.Location = new System.Drawing.Point(555, 114); label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label1.Name = "label1"; label1.Size = new System.Drawing.Size(56, 16); label1.TabIndex = 35; label1.Text = "Contact"; // // lnkContactEmail // lnkContactEmail.AutoSize = true; lnkContactEmail.Font = new System.Drawing.Font("Arial", 10F); lnkContactEmail.LinkColor = System.Drawing.Color.FromArgb(102, 153, 255); lnkContactEmail.Location = new System.Drawing.Point(555, 133); lnkContactEmail.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); lnkContactEmail.Name = "lnkContactEmail"; lnkContactEmail.Size = new System.Drawing.Size(163, 16); lnkContactEmail.TabIndex = 34; lnkContactEmail.TabStop = true; lnkContactEmail.Text = "support@technitium.com"; lnkContactEmail.VisitedLinkColor = System.Drawing.Color.White; lnkContactEmail.LinkClicked += lnkContactEmail_LinkClicked; // // labVersion // labVersion.AutoSize = true; labVersion.Font = new System.Drawing.Font("Arial", 12F); labVersion.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69); labVersion.Location = new System.Drawing.Point(100, 152); labVersion.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); labVersion.Name = "labVersion"; labVersion.Size = new System.Drawing.Size(102, 18); labVersion.TabIndex = 38; labVersion.Text = "version x.x.x.x"; // // label5 // label5.AutoSize = true; label5.Font = new System.Drawing.Font("Arial", 18F, System.Drawing.FontStyle.Bold); label5.ForeColor = System.Drawing.Color.FromArgb(45, 57, 69); label5.Location = new System.Drawing.Point(98, 119); label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label5.Name = "label5"; label5.Size = new System.Drawing.Size(206, 29); label5.TabIndex = 39; label5.Text = "System Tray App"; // // frmAbout // AcceptButton = btnClose; AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; BackColor = System.Drawing.Color.FromArgb(250, 250, 250); CancelButton = btnClose; ClientSize = new System.Drawing.Size(761, 301); Controls.Add(label5); Controls.Add(labVersion); Controls.Add(label3); Controls.Add(lnkWebsite); Controls.Add(label1); Controls.Add(lnkContactEmail); Controls.Add(label4); Controls.Add(lnkTerms); Controls.Add(btnClose); Controls.Add(label2); Controls.Add(panel1); FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); MaximizeBox = false; MinimizeBox = false; Name = "frmAbout"; StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; Text = "About Technitium DNS Server"; panel1.ResumeLayout(false); panel1.PerformLayout(); ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); ResumeLayout(false); PerformLayout(); } #endregion private System.Windows.Forms.Panel panel1; private System.Windows.Forms.PictureBox pictureBox1; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label4; private System.Windows.Forms.LinkLabel lnkTerms; private System.Windows.Forms.Button btnClose; private System.Windows.Forms.Label label3; private System.Windows.Forms.LinkLabel lnkWebsite; private System.Windows.Forms.Label label1; private System.Windows.Forms.LinkLabel lnkContactEmail; private System.Windows.Forms.Label labVersion; private System.Windows.Forms.Label label5; } } ================================================ FILE: DnsServerSystemTrayApp/frmAbout.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 System.Diagnostics; using System.Windows.Forms; namespace DnsServerSystemTrayApp { public partial class frmAbout : Form { public frmAbout() { InitializeComponent(); labVersion.Text = "version " + Application.ProductVersion; } private void lnkContactEmail_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { ProcessStartInfo processInfo = new ProcessStartInfo("mailto:" + lnkContactEmail.Text); processInfo.UseShellExecute = true; processInfo.Verb = "open"; Process.Start(processInfo); } private void lnkWebsite_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { ProcessStartInfo processInfo = new ProcessStartInfo(@"https://" + lnkWebsite.Text); processInfo.UseShellExecute = true; processInfo.Verb = "open"; Process.Start(processInfo); } private void lnkTerms_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { ProcessStartInfo processInfo = new ProcessStartInfo(@"https://go.technitium.com/?id=24"); processInfo.UseShellExecute = true; processInfo.Verb = "open"; Process.Start(processInfo); } } } ================================================ FILE: DnsServerSystemTrayApp/frmAbout.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 True True True True Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com) This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. Click link below for details: True True True True True True True True True AAABAAUAICAQAAAAAADoAgAAVgAAACAgAAAAAAAAqAgAAD4DAAAwMAAAAAAAAKgOAADmCwAAEBAQAAAA AAAoAQAAjhoAABAQAAAAAAAAaAUAALYbAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A /wD//wAA////AMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzP//zP//////////////zMz/ /8z//////////////8zM///M///////////////MzP//zP//////////////zMz//8zMzMzMzMz//8zM zMzM///MzMzMzMzM///MzMzMzP//////////zP//zP//zMz//////////8z//8z//8zM///////////M ///M///MzP//////////zP//zP//zMz//8zMzMzMzMz//8z//8zM///MzMzMzMzM///M///MzP//zP// zMzMzP//zP//zMz//8z//8zMzMz//8z//8zM///M///MzMzM///M///MzP//zP//zMzMzP//zP//zMz/ /8z//8zMzMzMzMz//8zM///M///MzMzMzMzM///MzP//zP//zP//////////zMz//8z//8z///////// /8zM///M///M///////////MzP//zP//zP//////////zMzMzMz//8zMzMzMzMz//8zMzMzM///MzMzM zMzM///MzP//////////////zP//zMz//////////////8z//8zM///////////////M///MzP////// ////////zP//zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAA AAABAAgAAAAAAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDA wADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIiIgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8 /wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAAmQAAAMwAADMAAAAzMwAAM2YAADOZAAAz zAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZMwAAmWYAAJmZAACZzAAAmf8AAMwAAADM MwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMAMwAzAGYAMwCZADMAzAAzAP8AMzMAADMz MwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNmzAAzZv8AM5kAADOZMwAzmWYAM5mZADOZ zAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/ZgAz/5kAM//MADP//wBmAAAAZgAzAGYA ZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz/wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZ AABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbMzABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA /wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkzzACZAP8AmWYAAJlmMwCZM2YAmWaZAJlm zACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbMZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnM ZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkzAADMMzMAzDNmAMwzmQDMM8wAzDP/AMxm AADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZmQDMmcwAzJn/AMzMAADMzDMAzMxmAMzM mQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwAMwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8z mQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+ZAAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/M AAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP//zABmZv8AZv9mAGb//wD/ZmYA/2b/AP// ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSg oACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1f/////V1f////////////////// ///////////V1dXV/////9XV/////////////////////////////9XV1dX/////1dX///////////// ////////////////1dXV1f/////V1f/////////////////////////////V1dXV/////9XV1dXV1dXV 1dXV1dXV/////9XV1dXV1dXV1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1dXV1dXV1f////////// ///////////V1f/////V1f/////V1dXV/////////////////////9XV/////9XV/////9XV1dX///// ////////////////1dX/////1dX/////1dXV1f/////////////////////V1f/////V1f/////V1dXV /////9XV1dXV1dXV1dXV1dXV/////9XV/////9XV1dX/////1dXV1dXV1dXV1dXV1dX/////1dX///// 1dXV1f/////V1f/////V1dXV1dXV1f/////V1f/////V1dXV/////9XV/////9XV1dXV1dXV/////9XV /////9XV1dX/////1dX/////1dXV1dXV1dX/////1dX/////1dXV1f/////V1f/////V1dXV1dXV1f// ///V1f/////V1dXV/////9XV/////9XV1dXV1dXV1dXV1dXV/////9XV1dX/////1dX/////1dXV1dXV 1dXV1dXV1dX/////1dXV1f/////V1f/////V1f/////////////////////V1dXV/////9XV/////9XV /////////////////////9XV1dX/////1dX/////1dX/////////////////////1dXV1f/////V1f// ///V1f/////////////////////V1dXV1dXV1dXV/////9XV1dXV1dXV1dXV1dXV/////9XV1dXV1dXV 1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1f/////////////////////////////V1f/////V1dXV /////////////////////////////9XV/////9XV1dX/////////////////////////////1dX///// 1dXV1f/////////////////////////////V1f/////V1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAwAAAAYAAAAAEA CAAAAAAAgAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDc wADwyqYABAQEAAgICAAMDAwAERERABYWFgAcHBwAIiIiACkpKQBVVVUATU1NAEJCQgA5OTkAgHz/AFBQ /wCTANYA/+zMAMbW7wDW5+cAkKmtAAAAMwAAAGYAAACZAAAAzAAAMwAAADMzAAAzZgAAM5kAADPMAAAz /wAAZgAAAGYzAABmZgAAZpkAAGbMAABm/wAAmQAAAJkzAACZZgAAmZkAAJnMAACZ/wAAzAAAAMwzAADM ZgAAzJkAAMzMAADM/wAA/2YAAP+ZAAD/zAAzAAAAMwAzADMAZgAzAJkAMwDMADMA/wAzMwAAMzMzADMz ZgAzM5kAMzPMADMz/wAzZgAAM2YzADNmZgAzZpkAM2bMADNm/wAzmQAAM5kzADOZZgAzmZkAM5nMADOZ /wAzzAAAM8wzADPMZgAzzJkAM8zMADPM/wAz/zMAM/9mADP/mQAz/8wAM///AGYAAABmADMAZgBmAGYA mQBmAMwAZgD/AGYzAABmMzMAZjNmAGYzmQBmM8wAZjP/AGZmAABmZjMAZmZmAGZmmQBmZswAZpkAAGaZ MwBmmWYAZpmZAGaZzABmmf8AZswAAGbMMwBmzJkAZszMAGbM/wBm/wAAZv8zAGb/mQBm/8wAzAD/AP8A zACZmQAAmTOZAJkAmQCZAMwAmQAAAJkzMwCZAGYAmTPMAJkA/wCZZgAAmWYzAJkzZgCZZpkAmWbMAJkz /wCZmTMAmZlmAJmZmQCZmcwAmZn/AJnMAACZzDMAZsxmAJnMmQCZzMwAmcz/AJn/AACZ/zMAmcxmAJn/ mQCZ/8wAmf//AMwAAACZADMAzABmAMwAmQDMAMwAmTMAAMwzMwDMM2YAzDOZAMwzzADMM/8AzGYAAMxm MwCZZmYAzGaZAMxmzACZZv8AzJkAAMyZMwDMmWYAzJmZAMyZzADMmf8AzMwAAMzMMwDMzGYAzMyZAMzM zADMzP8AzP8AAMz/MwCZ/2YAzP+ZAMz/zADM//8AzAAzAP8AZgD/AJkAzDMAAP8zMwD/M2YA/zOZAP8z zAD/M/8A/2YAAP9mMwDMZmYA/2aZAP9mzADMZv8A/5kAAP+ZMwD/mWYA/5mZAP+ZzAD/mf8A/8wAAP/M MwD/zGYA/8yZAP/MzAD/zP8A//8zAMz/ZgD//5kA///MAGZm/wBm/2YAZv//AP9mZgD/Zv8A//9mACEA pQBfX18Ad3d3AIaGhgCWlpYAy8vLALKysgDX19cA3d3dAOPj4wDq6uoA8fHxAPj4+ADw+/8ApKCgAICA gAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8A1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV//// ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV//////// ////////////////////////////////////1dXV1dXV////////1dXV//////////////////////// ////////////////////1dXV1dXV////////1dXV//////////////////////////////////////// ////1dXV1dXV////////1dXV////////////////////////////////////////////1dXV1dXV//// ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV1dXV1dXV 1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV ////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV 1dXV1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV//// ////////////////////////////1dXV////////1dXV////////1dXV1dXV//////////////////// ////////////1dXV////////1dXV////////1dXV1dXV////////////////////////////////1dXV ////////1dXV////////1dXV1dXV////////////////////////////////1dXV////////1dXV//// ////1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV//// ////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV 1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV//// ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV//// ////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV//////// 1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV//// ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV//// ////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV//////// 1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV////////1dXV//////////// ////////////////////1dXV1dXV////////1dXV////////1dXV//////////////////////////// ////1dXV1dXV////////1dXV////////1dXV////////////////////////////////1dXV1dXV//// ////1dXV////////1dXV////////////////////////////////1dXV1dXV////////1dXV//////// 1dXV////////////////////////////////1dXV1dXV////////1dXV////////1dXV//////////// ////////////////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV//// ////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV 1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV//////////////////// ////////////////////////1dXV////////1dXV1dXV//////////////////////////////////// ////////1dXV////////1dXV1dXV////////////////////////////////////////////1dXV//// ////1dXV1dXV////////////////////////////////////////////1dXV////////1dXV1dXV//// ////////////////////////////////////////1dXV////////1dXV1dXV//////////////////// ////////////////////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXVAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAKAAAABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAIAAAIAAAACAgACAAAAAgACAAICAAADAwMAAgICAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP// /wDMzMzMzMzMzM/8///////8z/z///////zP/MzMzP/MzM/////8/8/8z/////z/z/zP/MzMzP/P/M/8 /8zM/8/8z/z/zMz/z/zP/P/MzMzP/M/8/8/////8z/z/z/////zMzP/MzMzP/M///////8/8z/////// z/zMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA gAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIi IgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8/wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAA mQAAAMwAADMAAAAzMwAAM2YAADOZAAAzzAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZ MwAAmWYAAJmZAACZzAAAmf8AAMwAAADMMwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMA MwAzAGYAMwCZADMAzAAzAP8AMzMAADMzMwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNm zAAzZv8AM5kAADOZMwAzmWYAM5mZADOZzAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/ ZgAz/5kAM//MADP//wBmAAAAZgAzAGYAZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz /wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZAABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbM zABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA/wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkz zACZAP8AmWYAAJlmMwCZM2YAmWaZAJlmzACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbM ZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnMZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkz AADMMzMAzDNmAMwzmQDMM8wAzDP/AMxmAADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZ mQDMmcwAzJn/AMzMAADMzDMAzMxmAMzMmQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwA MwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8zmQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+Z AAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/MAAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP// zABmZv8AZv9mAGb//wD/ZmYA/2b/AP//ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d 3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSgoACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV 1dXV1dXV1dXV1dXV1dXV///V///////////////V1f//1f//////////////1dX//9XV1dXV1dX//9XV 1dXV///////////V///V///V1f//////////1f//1f//1dX//9XV1dXV1dX//9X//9XV///V///V1dXV ///V///V1f//1f//1dXV1f//1f//1dX//9X//9XV1dXV1dX//9XV///V///V///////////V1f//1f// 1f//////////1dXV1dX//9XV1dXV1dX//9XV///////////////V///V1f//////////////1f//1dXV 1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAA ================================================ FILE: DnsServerSystemTrayApp/frmManageDnsProviders.Designer.cs ================================================ namespace DnsServerSystemTrayApp { partial class frmManageDnsProviders { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frmManageDnsProviders)); this.listView1 = new System.Windows.Forms.ListView(); this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.columnHeader3 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.groupBox1 = new System.Windows.Forms.GroupBox(); this.btnDelete = new System.Windows.Forms.Button(); this.btnClear = new System.Windows.Forms.Button(); this.btnAddUpdate = new System.Windows.Forms.Button(); this.txtIpv6Addresses = new System.Windows.Forms.TextBox(); this.label3 = new System.Windows.Forms.Label(); this.txtIpv4Addresses = new System.Windows.Forms.TextBox(); this.label2 = new System.Windows.Forms.Label(); this.txtDnsProviderName = new System.Windows.Forms.TextBox(); this.label1 = new System.Windows.Forms.Label(); this.btnOK = new System.Windows.Forms.Button(); this.btnCancel = new System.Windows.Forms.Button(); this.btnRestoreDefaults = new System.Windows.Forms.Button(); this.groupBox1.SuspendLayout(); this.SuspendLayout(); // // listView1 // this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { this.columnHeader1, this.columnHeader2, this.columnHeader3}); this.listView1.FullRowSelect = true; this.listView1.HideSelection = false; this.listView1.Location = new System.Drawing.Point(12, 12); this.listView1.MultiSelect = false; this.listView1.Name = "listView1"; this.listView1.Size = new System.Drawing.Size(660, 200); this.listView1.Sorting = System.Windows.Forms.SortOrder.Ascending; this.listView1.TabIndex = 0; this.listView1.UseCompatibleStateImageBehavior = false; this.listView1.View = System.Windows.Forms.View.Details; this.listView1.SelectedIndexChanged += new System.EventHandler(this.listView1_SelectedIndexChanged); // // columnHeader1 // this.columnHeader1.Text = "DNS Provider"; this.columnHeader1.Width = 150; // // columnHeader2 // this.columnHeader2.Text = "IPv4 Addresses"; this.columnHeader2.Width = 240; // // columnHeader3 // this.columnHeader3.Text = "IPv6 Addresses"; this.columnHeader3.Width = 240; // // groupBox1 // this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.groupBox1.Controls.Add(this.btnDelete); this.groupBox1.Controls.Add(this.btnClear); this.groupBox1.Controls.Add(this.btnAddUpdate); this.groupBox1.Controls.Add(this.txtIpv6Addresses); this.groupBox1.Controls.Add(this.label3); this.groupBox1.Controls.Add(this.txtIpv4Addresses); this.groupBox1.Controls.Add(this.label2); this.groupBox1.Controls.Add(this.txtDnsProviderName); this.groupBox1.Controls.Add(this.label1); this.groupBox1.Location = new System.Drawing.Point(12, 216); this.groupBox1.Name = "groupBox1"; this.groupBox1.Size = new System.Drawing.Size(661, 130); this.groupBox1.TabIndex = 9; this.groupBox1.TabStop = false; // // btnDelete // this.btnDelete.Enabled = false; this.btnDelete.Location = new System.Drawing.Point(179, 97); this.btnDelete.Name = "btnDelete"; this.btnDelete.Size = new System.Drawing.Size(75, 23); this.btnDelete.TabIndex = 16; this.btnDelete.Text = "&Delete"; this.btnDelete.UseVisualStyleBackColor = true; this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click); // // btnClear // this.btnClear.Location = new System.Drawing.Point(260, 97); this.btnClear.Name = "btnClear"; this.btnClear.Size = new System.Drawing.Size(75, 23); this.btnClear.TabIndex = 17; this.btnClear.Text = "&Clear"; this.btnClear.UseVisualStyleBackColor = true; this.btnClear.Click += new System.EventHandler(this.btnClear_Click); // // btnAddUpdate // this.btnAddUpdate.Location = new System.Drawing.Point(98, 97); this.btnAddUpdate.Name = "btnAddUpdate"; this.btnAddUpdate.Size = new System.Drawing.Size(75, 23); this.btnAddUpdate.TabIndex = 15; this.btnAddUpdate.Text = "&Add"; this.btnAddUpdate.UseVisualStyleBackColor = true; this.btnAddUpdate.Click += new System.EventHandler(this.btnAddUpdate_Click); // // txtIpv6Addresses // this.txtIpv6Addresses.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.txtIpv6Addresses.Location = new System.Drawing.Point(98, 71); this.txtIpv6Addresses.MaxLength = 255; this.txtIpv6Addresses.Name = "txtIpv6Addresses"; this.txtIpv6Addresses.Size = new System.Drawing.Size(552, 20); this.txtIpv6Addresses.TabIndex = 14; // // label3 // this.label3.AutoSize = true; this.label3.Location = new System.Drawing.Point(11, 74); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(81, 13); this.label3.TabIndex = 13; this.label3.Text = "IPv6 Addresses"; // // txtIpv4Addresses // this.txtIpv4Addresses.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.txtIpv4Addresses.Location = new System.Drawing.Point(98, 45); this.txtIpv4Addresses.MaxLength = 255; this.txtIpv4Addresses.Name = "txtIpv4Addresses"; this.txtIpv4Addresses.Size = new System.Drawing.Size(552, 20); this.txtIpv4Addresses.TabIndex = 12; // // label2 // this.label2.AutoSize = true; this.label2.Location = new System.Drawing.Point(11, 48); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(81, 13); this.label2.TabIndex = 11; this.label2.Text = "IPv4 Addresses"; // // txtDnsProviderName // this.txtDnsProviderName.Location = new System.Drawing.Point(98, 19); this.txtDnsProviderName.MaxLength = 255; this.txtDnsProviderName.Name = "txtDnsProviderName"; this.txtDnsProviderName.Size = new System.Drawing.Size(200, 20); this.txtDnsProviderName.TabIndex = 10; // // label1 // this.label1.AutoSize = true; this.label1.Location = new System.Drawing.Point(20, 22); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(72, 13); this.label1.TabIndex = 9; this.label1.Text = "DNS Provider"; // // btnOK // this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK; this.btnOK.Location = new System.Drawing.Point(517, 356); this.btnOK.Name = "btnOK"; this.btnOK.Size = new System.Drawing.Size(75, 23); this.btnOK.TabIndex = 10; this.btnOK.Text = "OK"; this.btnOK.UseVisualStyleBackColor = true; // // btnCancel // this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.btnCancel.Location = new System.Drawing.Point(598, 356); this.btnCancel.Name = "btnCancel"; this.btnCancel.Size = new System.Drawing.Size(75, 23); this.btnCancel.TabIndex = 11; this.btnCancel.Text = "Cancel"; this.btnCancel.UseVisualStyleBackColor = true; // // btnRestoreDefaults // this.btnRestoreDefaults.Location = new System.Drawing.Point(12, 356); this.btnRestoreDefaults.Name = "btnRestoreDefaults"; this.btnRestoreDefaults.Size = new System.Drawing.Size(100, 23); this.btnRestoreDefaults.TabIndex = 12; this.btnRestoreDefaults.Text = "Restore &Defaults"; this.btnRestoreDefaults.UseVisualStyleBackColor = true; this.btnRestoreDefaults.Click += new System.EventHandler(this.btnRestoreDefaults_Click); // // frmManageDnsProviders // this.AcceptButton = this.btnOK; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.btnCancel; this.ClientSize = new System.Drawing.Size(684, 386); this.Controls.Add(this.btnRestoreDefaults); this.Controls.Add(this.btnCancel); this.Controls.Add(this.btnOK); this.Controls.Add(this.groupBox1); this.Controls.Add(this.listView1); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "frmManageDnsProviders"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "Manage Network DNS Providers - Technitium DNS Server"; this.Load += new System.EventHandler(this.frmManageDnsProviders_Load); this.groupBox1.ResumeLayout(false); this.groupBox1.PerformLayout(); this.ResumeLayout(false); } #endregion private System.Windows.Forms.ListView listView1; private System.Windows.Forms.ColumnHeader columnHeader1; private System.Windows.Forms.ColumnHeader columnHeader2; private System.Windows.Forms.ColumnHeader columnHeader3; private System.Windows.Forms.GroupBox groupBox1; private System.Windows.Forms.Button btnClear; private System.Windows.Forms.Button btnAddUpdate; private System.Windows.Forms.TextBox txtIpv6Addresses; private System.Windows.Forms.Label label3; private System.Windows.Forms.TextBox txtIpv4Addresses; private System.Windows.Forms.Label label2; private System.Windows.Forms.TextBox txtDnsProviderName; private System.Windows.Forms.Label label1; private System.Windows.Forms.Button btnOK; private System.Windows.Forms.Button btnCancel; private System.Windows.Forms.Button btnRestoreDefaults; private System.Windows.Forms.Button btnDelete; } } ================================================ FILE: DnsServerSystemTrayApp/frmManageDnsProviders.cs ================================================ /* Technitium DNS Server Copyright (C) 2024 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 System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Windows.Forms; namespace DnsServerSystemTrayApp { public partial class frmManageDnsProviders : Form { #region variables static readonly char[] commaSeparator = new char[] { ',' }; readonly List _dnsProviders = new List(); #endregion #region constructor public frmManageDnsProviders(ICollection dnsProviders) { InitializeComponent(); _dnsProviders.AddRange(dnsProviders); } #endregion #region private private void RefreshDnsProvidersList() { listView1.SuspendLayout(); listView1.Items.Clear(); foreach (DnsProvider dnsProvider in _dnsProviders) { ListViewItem item = listView1.Items.Add(dnsProvider.Name); item.SubItems.Add(dnsProvider.GetIpv4Addresses()); item.SubItems.Add(dnsProvider.GetIpv6Addresses()); item.Tag = dnsProvider; } listView1.ResumeLayout(); } private void ClearForm() { txtDnsProviderName.Text = ""; txtIpv4Addresses.Text = ""; txtIpv6Addresses.Text = ""; btnAddUpdate.Text = "Add"; btnDelete.Enabled = false; } private void frmManageDnsProviders_Load(object sender, EventArgs e) { RefreshDnsProvidersList(); } private void listView1_SelectedIndexChanged(object sender, EventArgs e) { if (listView1.SelectedItems.Count > 0) { ListViewItem selectedItem = listView1.SelectedItems[0]; txtDnsProviderName.Text = selectedItem.Text; txtIpv4Addresses.Text = selectedItem.SubItems[1].Text; txtIpv6Addresses.Text = selectedItem.SubItems[2].Text; btnAddUpdate.Text = "&Update"; btnDelete.Enabled = true; } else { ClearForm(); } } private void btnAddUpdate_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtDnsProviderName.Text)) { MessageBox.Show("Please enter a valid DNS Provider name.", "Missing DNS Provider!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } List addresses = new List(); foreach (string item in txtIpv4Addresses.Text.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries)) { if (IPAddress.TryParse(item.Trim(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork)) { addresses.Add(address); } else { MessageBox.Show("Please enter a valid IPv4 address.", "Invalid IPv4 Address!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } } foreach (string item in txtIpv6Addresses.Text.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries)) { if (IPAddress.TryParse(item.Trim(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6)) { addresses.Add(address); } else { MessageBox.Show("Please enter a valid IPv6 address.", "Invalid IPv6 Address!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } } if (addresses.Count == 0) { MessageBox.Show("Please enter at least one valid DNS provider IP address.", "Missing DNS Provider IP Address!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } if ((btnAddUpdate.Text != "Add") && (listView1.SelectedItems.Count > 0)) { ListViewItem selectedItem = listView1.SelectedItems[0]; DnsProvider dnsProvider = selectedItem.Tag as DnsProvider; dnsProvider.Name = txtDnsProviderName.Text.Trim(); dnsProvider.Addresses = addresses; } else { _dnsProviders.Add(new DnsProvider(txtDnsProviderName.Text.Trim(), addresses)); } RefreshDnsProvidersList(); ClearForm(); } private void btnDelete_Click(object sender, EventArgs e) { if (listView1.SelectedItems.Count > 0) { ListViewItem selectedItem = listView1.SelectedItems[0]; DnsProvider dnsProvider = selectedItem.Tag as DnsProvider; _dnsProviders.Remove(dnsProvider); listView1.Items.Remove(selectedItem); } RefreshDnsProvidersList(); ClearForm(); } private void btnClear_Click(object sender, EventArgs e) { ClearForm(); } private void btnRestoreDefaults_Click(object sender, EventArgs e) { _dnsProviders.Clear(); _dnsProviders.AddRange(DnsProvider.GetDefaultProviders()); RefreshDnsProvidersList(); ClearForm(); } #endregion #region properties public List DnsProviders { get { return _dnsProviders; } } #endregion } } ================================================ FILE: DnsServerSystemTrayApp/frmManageDnsProviders.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 True True True True True True True True True True True True True True True AAABAAUAICAQAAAAAADoAgAAVgAAACAgAAAAAAAAqAgAAD4DAAAwMAAAAAAAAKgOAADmCwAAEBAQAAAA AAAoAQAAjhoAABAQAAAAAAAAaAUAALYbAAAoAAAAIAAAAEAAAAABAAQAAAAAAIACAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwACAgIAAAAD/AAD/AAAA//8A/wAAAP8A /wD//wAA////AMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzP//zP//////////////zMz/ /8z//////////////8zM///M///////////////MzP//zP//////////////zMz//8zMzMzMzMz//8zM zMzM///MzMzMzMzM///MzMzMzP//////////zP//zP//zMz//////////8z//8z//8zM///////////M ///M///MzP//////////zP//zP//zMz//8zMzMzMzMz//8z//8zM///MzMzMzMzM///M///MzP//zP// zMzMzP//zP//zMz//8z//8zMzMz//8z//8zM///M///MzMzM///M///MzP//zP//zMzMzP//zP//zMz/ /8z//8zMzMzMzMz//8zM///M///MzMzMzMzM///MzP//zP//zP//////////zMz//8z//8z///////// /8zM///M///M///////////MzP//zP//zP//////////zMzMzMz//8zMzMzMzMz//8zMzMzM///MzMzM zMzM///MzP//////////////zP//zMz//////////////8z//8zM///////////////M///MzP////// ////////zP//zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAA AAABAAgAAAAAAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDA wADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIiIgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8 /wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAAmQAAAMwAADMAAAAzMwAAM2YAADOZAAAz zAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZMwAAmWYAAJmZAACZzAAAmf8AAMwAAADM MwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMAMwAzAGYAMwCZADMAzAAzAP8AMzMAADMz MwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNmzAAzZv8AM5kAADOZMwAzmWYAM5mZADOZ zAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/ZgAz/5kAM//MADP//wBmAAAAZgAzAGYA ZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz/wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZ AABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbMzABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA /wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkzzACZAP8AmWYAAJlmMwCZM2YAmWaZAJlm zACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbMZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnM ZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkzAADMMzMAzDNmAMwzmQDMM8wAzDP/AMxm AADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZmQDMmcwAzJn/AMzMAADMzDMAzMxmAMzM mQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwAMwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8z mQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+ZAAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/M AAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP//zABmZv8AZv9mAGb//wD/ZmYA/2b/AP// ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSg oACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1f/////V1f////////////////// ///////////V1dXV/////9XV/////////////////////////////9XV1dX/////1dX///////////// ////////////////1dXV1f/////V1f/////////////////////////////V1dXV/////9XV1dXV1dXV 1dXV1dXV/////9XV1dXV1dXV1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1dXV1dXV1f////////// ///////////V1f/////V1f/////V1dXV/////////////////////9XV/////9XV/////9XV1dX///// ////////////////1dX/////1dX/////1dXV1f/////////////////////V1f/////V1f/////V1dXV /////9XV1dXV1dXV1dXV1dXV/////9XV/////9XV1dX/////1dXV1dXV1dXV1dXV1dX/////1dX///// 1dXV1f/////V1f/////V1dXV1dXV1f/////V1f/////V1dXV/////9XV/////9XV1dXV1dXV/////9XV /////9XV1dX/////1dX/////1dXV1dXV1dX/////1dX/////1dXV1f/////V1f/////V1dXV1dXV1f// ///V1f/////V1dXV/////9XV/////9XV1dXV1dXV1dXV1dXV/////9XV1dX/////1dX/////1dXV1dXV 1dXV1dXV1dX/////1dXV1f/////V1f/////V1f/////////////////////V1dXV/////9XV/////9XV /////////////////////9XV1dX/////1dX/////1dX/////////////////////1dXV1f/////V1f// ///V1f/////////////////////V1dXV1dXV1dXV/////9XV1dXV1dXV1dXV1dXV/////9XV1dXV1dXV 1dX/////1dXV1dXV1dXV1dXV1dX/////1dXV1f/////////////////////////////V1f/////V1dXV /////////////////////////////9XV/////9XV1dX/////////////////////////////1dX///// 1dXV1f/////////////////////////////V1f/////V1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAwAAAAYAAAAAEA CAAAAAAAgAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDc wADwyqYABAQEAAgICAAMDAwAERERABYWFgAcHBwAIiIiACkpKQBVVVUATU1NAEJCQgA5OTkAgHz/AFBQ /wCTANYA/+zMAMbW7wDW5+cAkKmtAAAAMwAAAGYAAACZAAAAzAAAMwAAADMzAAAzZgAAM5kAADPMAAAz /wAAZgAAAGYzAABmZgAAZpkAAGbMAABm/wAAmQAAAJkzAACZZgAAmZkAAJnMAACZ/wAAzAAAAMwzAADM ZgAAzJkAAMzMAADM/wAA/2YAAP+ZAAD/zAAzAAAAMwAzADMAZgAzAJkAMwDMADMA/wAzMwAAMzMzADMz ZgAzM5kAMzPMADMz/wAzZgAAM2YzADNmZgAzZpkAM2bMADNm/wAzmQAAM5kzADOZZgAzmZkAM5nMADOZ /wAzzAAAM8wzADPMZgAzzJkAM8zMADPM/wAz/zMAM/9mADP/mQAz/8wAM///AGYAAABmADMAZgBmAGYA mQBmAMwAZgD/AGYzAABmMzMAZjNmAGYzmQBmM8wAZjP/AGZmAABmZjMAZmZmAGZmmQBmZswAZpkAAGaZ MwBmmWYAZpmZAGaZzABmmf8AZswAAGbMMwBmzJkAZszMAGbM/wBm/wAAZv8zAGb/mQBm/8wAzAD/AP8A zACZmQAAmTOZAJkAmQCZAMwAmQAAAJkzMwCZAGYAmTPMAJkA/wCZZgAAmWYzAJkzZgCZZpkAmWbMAJkz /wCZmTMAmZlmAJmZmQCZmcwAmZn/AJnMAACZzDMAZsxmAJnMmQCZzMwAmcz/AJn/AACZ/zMAmcxmAJn/ mQCZ/8wAmf//AMwAAACZADMAzABmAMwAmQDMAMwAmTMAAMwzMwDMM2YAzDOZAMwzzADMM/8AzGYAAMxm MwCZZmYAzGaZAMxmzACZZv8AzJkAAMyZMwDMmWYAzJmZAMyZzADMmf8AzMwAAMzMMwDMzGYAzMyZAMzM zADMzP8AzP8AAMz/MwCZ/2YAzP+ZAMz/zADM//8AzAAzAP8AZgD/AJkAzDMAAP8zMwD/M2YA/zOZAP8z zAD/M/8A/2YAAP9mMwDMZmYA/2aZAP9mzADMZv8A/5kAAP+ZMwD/mWYA/5mZAP+ZzAD/mf8A/8wAAP/M MwD/zGYA/8yZAP/MzAD/zP8A//8zAMz/ZgD//5kA///MAGZm/wBm/2YAZv//AP9mZgD/Zv8A//9mACEA pQBfX18Ad3d3AIaGhgCWlpYAy8vLALKysgDX19cA3d3dAOPj4wDq6uoA8fHxAPj4+ADw+/8ApKCgAICA gAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8A1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV//// ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV//////// ////////////////////////////////////1dXV1dXV////////1dXV//////////////////////// ////////////////////1dXV1dXV////////1dXV//////////////////////////////////////// ////1dXV1dXV////////1dXV////////////////////////////////////////////1dXV1dXV//// ////1dXV////////////////////////////////////////////1dXV1dXV////////1dXV1dXV1dXV 1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV ////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV 1dXV1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV//// ////////////////////////////1dXV////////1dXV////////1dXV1dXV//////////////////// ////////////1dXV////////1dXV////////1dXV1dXV////////////////////////////////1dXV ////////1dXV////////1dXV1dXV////////////////////////////////1dXV////////1dXV//// ////1dXV1dXV////////////////////////////////1dXV////////1dXV////////1dXV1dXV//// ////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV 1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV//// ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV//// ////1dXV////////1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV//////// 1dXV1dXV1dXV1dXV////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV ////////1dXV////////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV////////1dXV//// ////1dXV1dXV////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV//// ////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV//////// 1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV////////1dXV////////1dXV//////////// ////////////////////1dXV1dXV////////1dXV////////1dXV//////////////////////////// ////1dXV1dXV////////1dXV////////1dXV////////////////////////////////1dXV1dXV//// ////1dXV////////1dXV////////////////////////////////1dXV1dXV////////1dXV//////// 1dXV////////////////////////////////1dXV1dXV////////1dXV////////1dXV//////////// ////////////////////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV//// ////1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV1dXV 1dXV1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////1dXV1dXV//////////////////// ////////////////////////1dXV////////1dXV1dXV//////////////////////////////////// ////////1dXV////////1dXV1dXV////////////////////////////////////////////1dXV//// ////1dXV1dXV////////////////////////////////////////////1dXV////////1dXV1dXV//// ////////////////////////////////////////1dXV////////1dXV1dXV//////////////////// ////////////////////////1dXV////////1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV 1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXVAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAKAAAABAAAAAgAAAAAQAEAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAIAAAIAAAACAgACAAAAAgACAAICAAADAwMAAgICAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP// /wDMzMzMzMzMzM/8///////8z/z///////zP/MzMzP/MzM/////8/8/8z/////z/z/zP/MzMzP/P/M/8 /8zM/8/8z/z/zMz/z/zP/P/MzMzP/M/8/8/////8z/z/z/////zMzP/MzMzP/M///////8/8z/////// z/zMzMzMzMzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA gAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwADA3MAA8MqmAAQEBAAICAgADAwMABEREQAWFhYAHBwcACIi IgApKSkAVVVVAE1NTQBCQkIAOTk5AIB8/wBQUP8AkwDWAP/szADG1u8A1ufnAJCprQAAADMAAABmAAAA mQAAAMwAADMAAAAzMwAAM2YAADOZAAAzzAAAM/8AAGYAAABmMwAAZmYAAGaZAABmzAAAZv8AAJkAAACZ MwAAmWYAAJmZAACZzAAAmf8AAMwAAADMMwAAzGYAAMyZAADMzAAAzP8AAP9mAAD/mQAA/8wAMwAAADMA MwAzAGYAMwCZADMAzAAzAP8AMzMAADMzMwAzM2YAMzOZADMzzAAzM/8AM2YAADNmMwAzZmYAM2aZADNm zAAzZv8AM5kAADOZMwAzmWYAM5mZADOZzAAzmf8AM8wAADPMMwAzzGYAM8yZADPMzAAzzP8AM/8zADP/ ZgAz/5kAM//MADP//wBmAAAAZgAzAGYAZgBmAJkAZgDMAGYA/wBmMwAAZjMzAGYzZgBmM5kAZjPMAGYz /wBmZgAAZmYzAGZmZgBmZpkAZmbMAGaZAABmmTMAZplmAGaZmQBmmcwAZpn/AGbMAABmzDMAZsyZAGbM zABmzP8AZv8AAGb/MwBm/5kAZv/MAMwA/wD/AMwAmZkAAJkzmQCZAJkAmQDMAJkAAACZMzMAmQBmAJkz zACZAP8AmWYAAJlmMwCZM2YAmWaZAJlmzACZM/8AmZkzAJmZZgCZmZkAmZnMAJmZ/wCZzAAAmcwzAGbM ZgCZzJkAmczMAJnM/wCZ/wAAmf8zAJnMZgCZ/5kAmf/MAJn//wDMAAAAmQAzAMwAZgDMAJkAzADMAJkz AADMMzMAzDNmAMwzmQDMM8wAzDP/AMxmAADMZjMAmWZmAMxmmQDMZswAmWb/AMyZAADMmTMAzJlmAMyZ mQDMmcwAzJn/AMzMAADMzDMAzMxmAMzMmQDMzMwAzMz/AMz/AADM/zMAmf9mAMz/mQDM/8wAzP//AMwA MwD/AGYA/wCZAMwzAAD/MzMA/zNmAP8zmQD/M8wA/zP/AP9mAAD/ZjMAzGZmAP9mmQD/ZswAzGb/AP+Z AAD/mTMA/5lmAP+ZmQD/mcwA/5n/AP/MAAD/zDMA/8xmAP/MmQD/zMwA/8z/AP//MwDM/2YA//+ZAP// zABmZv8AZv9mAGb//wD/ZmYA/2b/AP//ZgAhAKUAX19fAHd3dwCGhoYAlpaWAMvLywCysrIA19fXAN3d 3QDj4+MA6urqAPHx8QD4+PgA8Pv/AKSgoACAgIAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////ANXV 1dXV1dXV1dXV1dXV1dXV///V///////////////V1f//1f//////////////1dX//9XV1dXV1dX//9XV 1dXV///////////V///V///V1f//////////1f//1f//1dX//9XV1dXV1dX//9X//9XV///V///V1dXV ///V///V1f//1f//1dXV1f//1f//1dX//9X//9XV1dXV1dX//9XV///V///V///////////V1f//1f// 1f//////////1dXV1dX//9XV1dXV1dX//9XV///////////////V///V1f//////////////1f//1dXV 1dXV1dXV1dXV1dXV1dUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAA ================================================ FILE: DnsServerWindowsService/DnsServerWindowsService.csproj ================================================  net9.0 false true true DnsServerWindowsService DnsService logo2.ico 14.3 false Shreyas Zare Technitium Technitium DNS Server https://technitium.com/dns/ https://github.com/TechnitiumSoftware/DnsServer DnsServerWindowsService ..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.Firewall.dll ================================================ FILE: DnsServerWindowsService/DnsServiceWorker.cs ================================================ /* 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; using Microsoft.Extensions.Hosting; using Microsoft.Win32; using System; using System.Reflection; using System.Threading; using System.Threading.Tasks; using TechnitiumLibrary.Net.Firewall; namespace DnsServerWindowsService { public sealed class DnsServiceWorker : BackgroundService { readonly DnsWebService _service; public DnsServiceWorker() { string configFolder = null; string[] args = Environment.GetCommandLineArgs(); if (args.Length == 2) configFolder = args[1]; _service = new DnsWebService(configFolder, new Uri("https://go.technitium.com/?id=43")); } public override async Task StartAsync(CancellationToken cancellationToken) { CheckFirewallEntries(); await _service.StartAsync(); } public override async Task StopAsync(CancellationToken cancellationToken) { await _service.StopAsync(); } public override void Dispose() { if (_service != null) _service.Dispose(); } protected override Task ExecuteAsync(CancellationToken stoppingToken) { return Task.CompletedTask; } private static void CheckFirewallEntries() { bool autoFirewallEntry = true; try { #pragma warning disable CA1416 // Validate platform compatibility using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Technitium\DNS Server", false)) { if (key is not null) autoFirewallEntry = Convert.ToInt32(key.GetValue("AutoFirewallEntry", 1)) == 1; } #pragma warning restore CA1416 // Validate platform compatibility } catch { } if (autoFirewallEntry) { string appPath = Assembly.GetEntryAssembly().Location; if (appPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) appPath = appPath.Substring(0, appPath.Length - 4) + ".exe"; if (!WindowsFirewallEntryExists(appPath)) AddWindowsFirewallEntry(appPath); } } private static bool WindowsFirewallEntryExists(string appPath) { try { return WindowsFirewall.RuleExistsVista("", appPath) == RuleStatus.Allowed; } catch { return false; } } private static bool AddWindowsFirewallEntry(string appPath) { try { RuleStatus status = WindowsFirewall.RuleExistsVista("", appPath); switch (status) { case RuleStatus.Blocked: case RuleStatus.Disabled: WindowsFirewall.RemoveRuleVista("", appPath); break; case RuleStatus.Allowed: return true; } WindowsFirewall.AddRuleVista("Technitium DNS Server", "Allows incoming connection request to the DNS server.", FirewallAction.Allow, appPath, Protocol.ANY, null, null, null, null, InterfaceTypeFlags.All, true, Direction.Inbound, true); return true; } catch { return false; } } } } ================================================ FILE: DnsServerWindowsService/Program.cs ================================================ /* Technitium DNS Server Copyright (C) 2021 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 Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace DnsServerWindowsService { static class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddHostedService(); }) .UseWindowsService(); } } } ================================================ FILE: DnsServerWindowsService/Properties/PublishProfiles/FolderProfile.pubxml ================================================  Release Any CPU ..\DnsServerWindowsSetup\publish FileSystem <_TargetId>Folder net9.0 false ================================================ FILE: DnsServerWindowsSetup/DnsServerSetup.iss ================================================ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Technitium DNS Server" #define MyAppVersion "14.3" #define MyAppPublisher "Technitium" #define MyAppURL "https://technitium.com/dns/" #define MyAppExeName "DnsServerSystemTrayApp.exe" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{1052DB5E-35BD-4F67-89CD-1F45A1688E77} AppName={#MyAppName} AppVersion={#MyAppVersion} ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} VersionInfoVersion=2.2.0.0 VersionInfoCopyright="Copyright (C) 2025 Technitium" DefaultDirName={commonpf32}\Technitium\DNS Server DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes PrivilegesRequired=admin OutputDir=.\Release OutputBaseFilename=DnsServerSetup SetupIconFile=.\logo.ico WizardSmallImageFile=.\logo.bmp Compression=lzma SolidCompression=yes WizardStyle=modern UninstallDisplayIcon={app}\{#MyAppExeName} [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; [Files] Source: ".\publish\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: ".\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{group}\DNS Server App"; Filename: "{app}\{#MyAppExeName}" Name: "{group}\Dashboard"; Filename: "http://localhost:5380/" Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{autodesktop}\DNS Server App"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Parameters: "--first-run"; Description: "{cm:LaunchProgram,{#StringChange("DNS Server App", '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser #include "helper.iss" #include "legacy.iss" #include "dotnet.iss" #include "appinstall.iss" [Code] { Skips the tasks page if it is an upgrade install } function ShouldSkipPage(PageID: Integer): Boolean; begin Result := ((PageID = wpSelectTasks) or (PageID = wpSelectDir)) and (IsLegacyInstallerInstalled or IsUpgrade); end; function InitializeSetup: Boolean; begin CheckDotnetDependency; Result := true; end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssInstall then begin //Step happens just before installing files WizardForm.StatusLabel.Caption := 'Stopping Tray App...'; KillTrayApp(); //Stop the tray app if running if IsLegacyInstallerInstalled then begin WizardForm.StatusLabel.Caption := 'Stopping Service...'; DoStopService(); //Stop the service if running WizardForm.StatusLabel.Caption := 'Removing Legacy Installer...'; UninstallLegacyInstaller(); //Uninstall Legacy Installer if Installed already end else begin WizardForm.StatusLabel.Caption := 'Uninstalling Service...'; DoRemoveService(); //Stop and remove the service if installed end; end; if CurStep = ssPostInstall then begin //Step happens just after installing files WizardForm.StatusLabel.Caption := 'Installing Service...'; DoInstallService(); //Install service after all files installed, if not a portable install end; end; procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); begin if CurUninstallStep = usUninstall then //Step happens before processing uninstall log begin UninstallProgressForm.StatusLabel.Caption := 'Resetting Network DNS...'; ResetNetworkDNS(); //Reset Network DNS to default UninstallProgressForm.StatusLabel.Caption := 'Stopping Tray App...'; KillTrayApp(); //Stop the tray app if running UninstallProgressForm.StatusLabel.Caption := 'Uninstalling Service...'; DoRemoveService(); //Stop and remove the service end; end; ================================================ FILE: DnsServerWindowsSetup/appinstall.iss ================================================ #include "service.iss" #define SERVICE_NAME "DnsService" #define SERVICE_FILE "DnsService.exe" #define SERVICE_DISPLAY_NAME "Technitium DNS Server" #define SERVICE_DESCRIPTION "Technitium DNS Server" #define TRAYAPP_FILENAME "DnsServerSystemTrayApp.exe" [Code] { Kills the tray app } procedure KillTrayApp; begin TaskKill('{#TRAYAPP_FILENAME}'); end; { Resets Network DNS to default } procedure ResetNetworkDNS; var ResultCode: Integer; begin Exec(ExpandConstant('{app}\{#TRAYAPP_FILENAME}'), '--network-dns-default-exit', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; { Stops the service } procedure DoStopService(); var stopCounter: Integer; begin stopCounter := 0; if IsServiceInstalled('{#SERVICE_NAME}') then begin Log('Service: Already installed'); if IsServiceRunning('{#SERVICE_NAME}') then begin Log('Service: Already running, stopping service...'); StopService('{#SERVICE_NAME}'); while IsServiceRunning('{#SERVICE_NAME}') do begin if stopCounter > 2 then begin Log('Service: Waited too long to stop, killing task...'); TaskKill('{#SERVICE_FILE}'); Log('Service: Task killed'); break; end else begin Log('Service: Waiting for stop'); Sleep(2000); stopCounter := stopCounter + 1 end; end; if stopCounter < 3 then Log('Service: Stopped'); end; end; end; { Removes the service from the computer } procedure DoRemoveService(); var stopCounter: Integer; begin stopCounter := 0; if IsServiceInstalled('{#SERVICE_NAME}') then begin Log('Service: Already installed, begin remove...'); if IsServiceRunning('{#SERVICE_NAME}') then begin Log('Service: Already running, stopping...'); StopService('{#SERVICE_NAME}'); while IsServiceRunning('{#SERVICE_NAME}') do begin if stopCounter > 2 then begin Log('Service: Waited too long to stop, killing task...'); TaskKill('{#SERVICE_FILE}'); Log('Service: Task killed'); break; end else begin Log('Service: Waiting for stop'); Sleep(5000); stopCounter := stopCounter + 1 end; end; end; stopCounter := 0; Log('Service: Removing...'); RemoveService('{#SERVICE_NAME}'); while IsServiceInstalled('{#SERVICE_NAME}') do begin if stopCounter > 2 then begin Log('Service: Waited too long to remove, continuing'); break; end else begin Log('Service: Waiting for removal'); Sleep(5000); stopCounter := stopCounter + 1 end; end; if stopCounter < 3 then Log('Service: Removed'); end; end; { Installs the service onto the computer } procedure DoInstallService(); var InstallSuccess: Boolean; stopCounter: Integer; begin stopCounter := 0; if IsServiceInstalled('{#SERVICE_NAME}') then begin Log('Service: Already installed, skip install service'); end else begin Log('Service: Begin Install'); InstallSuccess := InstallService(ExpandConstant('"{app}\DnsService.exe"'), '{#SERVICE_NAME}', '{#SERVICE_DISPLAY_NAME}', '{#SERVICE_DESCRIPTION}', SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START); if not InstallSuccess then begin Log('Service: Install Fail ' + ServiceErrorToMessage(GetLastError())); SuppressibleMsgBox(ExpandConstant('{cm:ServiceInstallFailure,' + ServiceErrorToMessage(GetLastError()) + '}'), mbCriticalError, MB_OK, IDOK); end else begin Log('Service: Install Success, Starting...'); StartService('{#SERVICE_NAME}'); while IsServiceRunning('{#SERVICE_NAME}') <> true do begin if stopCounter > 3 then begin Log('Service: Waited too long to start, continue'); break; end else begin Log('Service: still starting') Sleep(5000); stopCounter := stopCounter + 1 end; end; if stopCounter < 4 then Log('Service: Started'); end; end; end; ================================================ FILE: DnsServerWindowsSetup/dotnet.iss ================================================ [Setup] MinVersion=6.1sp1 // remove next line if you only deploy 32-bit binaries and dependencies ArchitecturesInstallIn64BitMode=x64 // dependency installation requires ready page and ready memo to be enabled (default behaviour) DisableReadyPage=no DisableReadyMemo=no // shared code for installing the dependencies [Code] // types and variables type TDependency = record Filename: String; Parameters: String; Title: String; URL: String; Checksum: String; ForceSuccess: Boolean; InstallClean: Boolean; RebootAfter: Boolean; end; InstallResult = (InstallSuccessful, InstallRebootRequired, InstallError); var MemoInstallInfo: String; Dependencies: array of TDependency; DelayedReboot, ForceX86: Boolean; DownloadPage: TDownloadWizardPage; procedure AddDependency(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, InstallClean, RebootAfter: Boolean); var Dependency: TDependency; I: Integer; begin MemoInstallInfo := MemoInstallInfo + #13#10 + '%1' + Title; Dependency.Filename := Filename; Dependency.Parameters := Parameters; Dependency.Title := Title; if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin Dependency.URL := ''; end else begin Dependency.URL := URL; end; Dependency.Checksum := Checksum; Dependency.ForceSuccess := ForceSuccess; Dependency.InstallClean := InstallClean; Dependency.RebootAfter := RebootAfter; I := GetArrayLength(Dependencies); SetArrayLength(Dependencies, I + 1); Dependencies[I] := Dependency; end; function IsPendingReboot: Boolean; var Value: String; begin Result := RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager', 'PendingFileRenameOperations', Value) or (RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager', 'SetupExecute', Value) and (Value <> '')); end; function InstallProducts: InstallResult; var ResultCode, I, ProductCount: Integer; begin Result := InstallSuccessful; ProductCount := GetArrayLength(Dependencies); MemoInstallInfo := SetupMessage(msgReadyMemoTasks); if ProductCount > 0 then begin DownloadPage.Show; for I := 0 to ProductCount - 1 do begin if Dependencies[I].InstallClean and (DelayedReboot or IsPendingReboot) then begin Result := InstallRebootRequired; break; end; DownloadPage.SetText(Dependencies[I].Title, ''); DownloadPage.SetProgress(I + 1, ProductCount); while True do begin ResultCode := 0; if ShellExec('', ExpandConstant('{tmp}{\}') + Dependencies[I].Filename, Dependencies[I].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin if Dependencies[I].RebootAfter then begin // delay reboot after install if we installed the last dependency anyways if I = ProductCount - 1 then begin DelayedReboot := True; end else begin Result := InstallRebootRequired; MemoInstallInfo := Dependencies[I].Title; end; break; end else if (ResultCode = 0) or Dependencies[I].ForceSuccess then begin break; end else if ResultCode = 3010 then begin // Windows Installer ResultCode 3010: ERROR_SUCCESS_REBOOT_REQUIRED DelayedReboot := True; break; end; end; case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependencies[I].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of IDABORT: begin Result := InstallError; MemoInstallInfo := MemoInstallInfo + #13#10 + ' ' + Dependencies[I].Title; break; end; IDIGNORE: begin MemoInstallInfo := MemoInstallInfo + #13#10 + ' ' + Dependencies[I].Title; break; end; end; end; if Result <> InstallSuccessful then begin break; end; end; DownloadPage.Hide; end; end; // Inno Setup event functions procedure InitializeWizard; begin DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); end; function PrepareToInstall(var NeedsRestart: Boolean): String; begin DelayedReboot := False; case InstallProducts of InstallError: begin Result := MemoInstallInfo; end; InstallRebootRequired: begin Result := MemoInstallInfo; NeedsRestart := True; // write into the registry that the installer needs to be executed again after restart RegWriteStringValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InstallBootstrap', ExpandConstant('{srcexe}')); end; end; end; function NeedRestart: Boolean; begin Result := DelayedReboot; end; function UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; begin Result := ''; if MemoUserInfoInfo <> '' then begin Result := Result + MemoUserInfoInfo + Newline + NewLine; end; if MemoDirInfo <> '' then begin Result := Result + MemoDirInfo + Newline + NewLine; end; if MemoTypeInfo <> '' then begin Result := Result + MemoTypeInfo + Newline + NewLine; end; if MemoComponentsInfo <> '' then begin Result := Result + MemoComponentsInfo + Newline + NewLine; end; if MemoGroupInfo <> '' then begin Result := Result + MemoGroupInfo + Newline + NewLine; end; if MemoTasksInfo <> '' then begin Result := Result + MemoTasksInfo; end; if MemoInstallInfo <> '' then begin if MemoTasksInfo = '' then begin Result := Result + SetupMessage(msgReadyMemoTasks); end; Result := Result + FmtMessage(MemoInstallInfo, [Space]); end; end; function NextButtonClick(const CurPageID: Integer): Boolean; var I, ProductCount: Integer; Retry: Boolean; begin Result := True; if (CurPageID = wpReady) and (MemoInstallInfo <> '') then begin DownloadPage.Show; ProductCount := GetArrayLength(Dependencies); for I := 0 to ProductCount - 1 do begin if Dependencies[I].URL <> '' then begin DownloadPage.Clear; DownloadPage.Add(Dependencies[I].URL, Dependencies[I].Filename, Dependencies[I].Checksum); Retry := True; while Retry do begin Retry := False; try DownloadPage.Download; except if GetExceptionMessage = SetupMessage(msgErrorDownloadAborted) then begin Result := False; I := ProductCount; end else begin case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of IDABORT: begin Result := False; I := ProductCount; end; IDRETRY: begin Retry := True; end; end; end; end; end; end; end; DownloadPage.Hide; end; end; // architecture helper functions function IsX64: Boolean; begin Result := not ForceX86 and Is64BitInstallMode; end; function GetString(const x86, x64: String): String; begin if IsX64 then begin Result := x64; end else begin Result := x86; end; end; function GetArchitectureSuffix: String; begin Result := GetString('', '_x64'); end; function GetArchitectureTitle: String; begin Result := GetString(' (x86)', ' (x64)'); end; function CompareVersion(const Version1, Version2: String): Integer; var Position, Number1, Number2: Integer; begin Result := 0; while (Version1 <> '') or (Version2 <> '') do begin Position := Pos('.', Version1); if Position > 0 then begin Number1 := StrToIntDef(Copy(Version1, 1, Position - 1), 0); Delete(Version1, 1, Position); end else if Version1 <> '' then begin Number1 := StrToIntDef(Version1, 0); Version1 := ''; end else begin Number1 := 0; end; Position := Pos('.', Version2); if Position > 0 then begin Number2 := StrToIntDef(Copy(Version2, 1, Position - 1), 0); Delete(Version2, 1, Position); end else if Version2 <> '' then begin Number2 := StrToIntDef(Version2, 0); Version2 := ''; end else begin Number2 := 0; end; if Number1 < Number2 then begin Result := -1; break; end else if Number1 > Number2 then begin Result := 1; break; end; end; end; { Check if dotnet is installed } function IsAspDotNetInstalled: Boolean; var ResultCode: Integer; begin Result := false; Exec('cmd.exe', '/c dotnet --list-runtimes | find /n "Microsoft.AspNetCore.App 9.0.11"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); if ResultCode = 0 then begin Result := true; end; end; function IsDotNetDesktopInstalled: Boolean; var ResultCode: Integer; begin Result := false; Exec('cmd.exe', '/c dotnet --list-runtimes | find /n "Microsoft.WindowsDesktop.App 9.0.11"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); if ResultCode = 0 then begin Result := true; end; end; { if dotnet is not installed then add it for download } procedure CheckDotnetDependency; begin if not IsAspDotNetInstalled then begin AddDependency('aspdotnet80' + GetArchitectureSuffix + '.exe', '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', 'ASP.NET Core Runtime 9.0.11' + GetArchitectureTitle, GetString('https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.11/aspnetcore-runtime-9.0.11-win-x86.exe', 'https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.11/aspnetcore-runtime-9.0.11-win-x64.exe'), '', False, False, False); end; if not IsDotNetDesktopInstalled then begin AddDependency('dotnet80desktop' + GetArchitectureSuffix + '.exe', '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', '.NET Desktop Runtime 9.0.11' + GetArchitectureTitle, GetString('https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/9.0.11/windowsdesktop-runtime-9.0.11-win-x86.exe', 'https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/9.0.11/windowsdesktop-runtime-9.0.11-win-x64.exe'), '', False, False, False); end; end; ================================================ FILE: DnsServerWindowsSetup/helper.iss ================================================ [Code] { Helper functions } { Checks to see if the installer is an 'upgrade' } function IsUpgrade: Boolean; var Value: string; UninstallKey: string; begin UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + ExpandConstant('{#SetupSetting("AppId")}') + '_is1'; Result := (RegQueryStringValue(HKLM, UninstallKey, 'UninstallString', Value) or RegQueryStringValue(HKCU, UninstallKey, 'UninstallString', Value)) and (Value <> ''); end; { Kills a running program by its filename } procedure TaskKill(fileName: String); var ResultCode: Integer; begin Exec(ExpandConstant('taskkill.exe'), '/f /im ' + '"' + fileName + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; { Executes the MSI Uninstall by GUID functionality } function MsiExecUnins(appId: String): Integer; var ResultCode: Integer; begin ShellExec('', 'msiexec.exe', '/x ' + appId + ' /norestart /qb', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Result := ResultCode; end; ================================================ FILE: DnsServerWindowsSetup/legacy.iss ================================================ #define LEGACY_INSTALLER_APPID "{9B86AC7F-53B3-4E31-B245-D4602D16F5C8}" [Code] { Legacy Installer Functionality } { Checks if the MSI Installer is installed } function IsLegacyInstallerInstalled: Boolean; var Value: string; UninstallKey1, UninstallKey2: string; begin UninstallKey1 := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#LEGACY_INSTALLER_APPID}'; UninstallKey2 := 'SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{#LEGACY_INSTALLER_APPID}'; Result := ( RegQueryStringValue(HKLM, UninstallKey1, 'UninstallString', Value) or RegQueryStringValue(HKCU, UninstallKey1, 'UninstallString', Value) or RegQueryStringValue(HKLM, UninstallKey2, 'UninstallString', Value) ) and (Value <> ''); end; { Uninstalls Legacy Installer } procedure UninstallLegacyInstaller; var ResultCode: Integer; begin Log('Uninstall MSI installer item'); ResultCode := MsiExecUnins('{#LEGACY_INSTALLER_APPID}'); Log('Result code ' + IntToStr(ResultCode)); end; ================================================ FILE: DnsServerWindowsSetup/service.iss ================================================ [Code] type SERVICE_STATUS = record dwServiceType : cardinal; dwCurrentState : cardinal; dwControlsAccepted : cardinal; dwWin32ExitCode : cardinal; dwServiceSpecificExitCode : cardinal; dwCheckPoint : cardinal; dwWaitHint : cardinal; end; HANDLE = cardinal; const SERVICE_QUERY_CONFIG = $1; SERVICE_CHANGE_CONFIG = $2; SERVICE_QUERY_STATUS = $4; SERVICE_START = $10; SERVICE_STOP = $20; SERVICE_ALL_ACCESS = $f01ff; SC_MANAGER_ALL_ACCESS = $f003f; SERVICE_WIN32_OWN_PROCESS = $10; SERVICE_WIN32_SHARE_PROCESS = $20; SERVICE_WIN32 = $30; SERVICE_INTERACTIVE_PROCESS = $100; SERVICE_BOOT_START = $0; SERVICE_SYSTEM_START = $1; SERVICE_AUTO_START = $2; SERVICE_DEMAND_START = $3; SERVICE_DISABLED = $4; SERVICE_DELETE = $10000; SERVICE_CONTROL_STOP = $1; SERVICE_CONTROL_PAUSE = $2; SERVICE_CONTROL_CONTINUE = $3; SERVICE_CONTROL_INTERROGATE = $4; SERVICE_STOPPED = $1; SERVICE_START_PENDING = $2; SERVICE_STOP_PENDING = $3; SERVICE_RUNNING = $4; SERVICE_CONTINUE_PENDING = $5; SERVICE_PAUSE_PENDING = $6; SERVICE_PAUSED = $7; ERROR_ACCESS_DENIED = 5; ERROR_CIRCULAR_DEPENDENCY = 1059; ERROR_DUPLICATE_SERVICE_NAME = 1078; ERROR_INVALID_HANDLE = 6; ERROR_INVALID_NAME = 123; ERROR_INVALID_PARAMETER = 87; ERROR_INVALID_SERVICE_ACCOUNT = 1057; ERROR_SERVICE_EXISTS = 1073; ERROR_SERVICE_MARKED_FOR_DELETE = 1072; // ####################################################################################### // nt based service utilities // ####################################################################################### function OpenSCManager(lpMachineName, lpDatabaseName: string; dwDesiredAccess :cardinal): HANDLE; external 'OpenSCManagerW@advapi32.dll stdcall'; function OpenService(hSCManager :HANDLE;lpServiceName: string; dwDesiredAccess :cardinal): HANDLE; external 'OpenServiceW@advapi32.dll stdcall'; function CloseServiceHandle(hSCObject :HANDLE): boolean; external 'CloseServiceHandle@advapi32.dll stdcall'; function CreateService(hSCManager :HANDLE;lpServiceName, lpDisplayName: string;dwDesiredAccess,dwServiceType,dwStartType,dwErrorControl: cardinal;lpBinaryPathName,lpLoadOrderGroup: String; lpdwTagId : cardinal;lpDependencies,lpServiceStartName,lpPassword :string): cardinal; external 'CreateServiceW@advapi32.dll stdcall'; function DeleteService(hService :HANDLE): boolean; external 'DeleteService@advapi32.dll stdcall'; function StartNTService(hService :HANDLE;dwNumServiceArgs : cardinal;lpServiceArgVectors : cardinal) : boolean; external 'StartServiceW@advapi32.dll stdcall'; function ControlService(hService :HANDLE; dwControl :cardinal;var ServiceStatus :SERVICE_STATUS) : boolean; external 'ControlService@advapi32.dll stdcall'; function QueryServiceStatus(hService :HANDLE;var ServiceStatus :SERVICE_STATUS) : boolean; external 'QueryServiceStatus@advapi32.dll stdcall'; function QueryServiceStatusEx(hService :HANDLE;ServiceStatus :SERVICE_STATUS) : boolean; external 'QueryServiceStatus@advapi32.dll stdcall'; function GetLastError(): dword; external 'GetLastError@kernel32.dll stdcall'; function OpenServiceManager(): HANDLE; begin if UsingWinNT() = true then begin Result := OpenSCManager('', 'ServicesActive', SC_MANAGER_ALL_ACCESS); if Result = 0 then MsgBox(ExpandConstant('{cm:ServiceManagerUnavailable}'), mbError, MB_OK); end else begin MsgBox('only nt based systems support services', mbError, MB_OK); Result := 0; end end; function IsServiceInstalled(ServiceName: string): boolean; var hSCM : HANDLE; hService: HANDLE; begin hSCM := OpenServiceManager(); Result := false; if hSCM <> 0 then begin hService := OpenService(hSCM, ServiceName, SERVICE_QUERY_CONFIG); if hService <> 0 then begin Result := true; CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end end; function InstallService(FileName, ServiceName, DisplayName, Description: string; ServiceType, StartType: cardinal): boolean; var hSCM : HANDLE; hService: HANDLE; begin hSCM := OpenServiceManager(); Result := false; if hSCM <> 0 then begin hService := CreateService(hSCM, ServiceName, DisplayName, SERVICE_ALL_ACCESS, ServiceType, StartType, 0, FileName,'', 0, '', '', ''); if hService <> 0 then begin Result := true; // Win2K & WinXP supports aditional description text for services if Description <> '' then RegWriteStringValue(HKLM,'System\CurrentControlSet\Services\' + ServiceName, 'Description', Description); CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end; end; function RemoveService(ServiceName: string): boolean; var hSCM : HANDLE; hService: HANDLE; begin hSCM := OpenServiceManager(); Result := false; if hSCM <> 0 then begin hService := OpenService(hSCM, ServiceName, SERVICE_DELETE); if hService <> 0 then begin Result := DeleteService(hService); CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end; end; function StartService(ServiceName: string): boolean; var hSCM : HANDLE; hService: HANDLE; begin hSCM := OpenServiceManager(); Result := false; if hSCM <> 0 then begin hService := OpenService(hSCM, ServiceName, SERVICE_START); if hService <> 0 then begin Result := StartNTService(hService, 0, 0); CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end; end; function StopService(ServiceName: string): boolean; var hSCM : HANDLE; hService: HANDLE; Status : SERVICE_STATUS; begin hSCM := OpenServiceManager(); Result := false; if hSCM <> 0 then begin hService := OpenService(hSCM, ServiceName, SERVICE_STOP); if hService <> 0 then begin Result := ControlService(hService, SERVICE_CONTROL_STOP, Status); CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end; end; function IsServiceRunning(ServiceName: string): boolean; var hSCM : HANDLE; hService: HANDLE; Status : SERVICE_STATUS; begin hSCM := OpenServiceManager(); Result := false; if hSCM <> 0 then begin hService := OpenService(hSCM, ServiceName, SERVICE_QUERY_STATUS); if hService <> 0 then begin if QueryServiceStatus(hService, Status) then begin Result :=(Status.dwCurrentState = SERVICE_RUNNING); end; CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end end; function ServiceErrorToMessage(Error: word): string; begin case Error of ERROR_ACCESS_DENIED: Result := 'Access Denied'; ERROR_CIRCULAR_DEPENDENCY: Result := 'Circular Dependency'; ERROR_DUPLICATE_SERVICE_NAME: Result := 'Duplicate Service Name'; ERROR_INVALID_HANDLE: Result := 'Invalid Handle'; ERROR_INVALID_NAME: Result := 'Invalid Name'; ERROR_INVALID_PARAMETER: Result := 'Invalid Parameter'; ERROR_INVALID_SERVICE_ACCOUNT: Result := 'Invalid Service Account'; ERROR_SERVICE_EXISTS: Result := 'Service Exists'; ERROR_SERVICE_MARKED_FOR_DELETE: Result := 'Service Marked For Deletion'; else Result := 'Unknown error: ' + IntToStr(Error); end; end; ================================================ FILE: DockerEnvironmentVariables.md ================================================ # Technitium DNS Server Docker Environment Variables Technitium DNS Server supports environment variables to allow initializing the config when the DNS server starts for the first time. These environment variables are useful for creating docker container and can be used as shown in the [docker-compose.yml](https://github.com/TechnitiumSoftware/DnsServer/blob/master/docker-compose.yml) file. NOTE! These environment variables are read by the DNS server only when the DNS config file does not exists i.e. when the DNS server starts for the first time. The environment variables are described below: | Environment Variable | Type | Description | | ---------------------------------------------- | ------- | -----------------------------------------------------------------------------------------------------------------------------------------| | DNS_SERVER_DOMAIN | String | The primary domain name used by this DNS Server to identify itself. | | DNS_SERVER_ADMIN_PASSWORD | String | The DNS web console admin user password. | | DNS_SERVER_ADMIN_PASSWORD_FILE | String | The path to a file that contains a plain text password for the DNS web console admin user. | | DNS_SERVER_PREFER_IPV6 | Boolean | DNS Server will use IPv6 for querying whenever possible with this option enabled. | | DNS_SERVER_WEB_SERVICE_LOCAL_ADDRESSES | String | A comma separated list of IP addresses for the DNS web console to listen on. | | DNS_SERVER_WEB_SERVICE_HTTP_PORT | Integer | The TCP port number for the DNS web console over HTTP protocol. | | DNS_SERVER_WEB_SERVICE_HTTPS_PORT | Integer | The TCP port number for the DNS web console over HTTPS protocol. | | DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS | Boolean | Enables HTTPS for the DNS web console. | | DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT | Boolean | Enables self signed TLS certificate for the DNS web console. | | DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH | String | The file path to the TLS certificate for the DNS web console. | | DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD| String | The password for the TLS certificate for the DNS web console. | | DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT | Boolean | Enables HTTP to HTTPS redirection for the DNS web console. | | DNS_SERVER_OPTIONAL_PROTOCOL_DNS_OVER_HTTP | Boolean | Enables DNS server optional protocol DNS-over-HTTP on TCP port 80 to be used with a TLS terminating reverse proxy like nginx. | | DNS_SERVER_RECURSION | String | Recursion options: `Allow`, `Deny`, `AllowOnlyForPrivateNetworks`, `UseSpecifiedNetworkACL`. | | DNS_SERVER_RECURSION_NETWORK_ACL | String | A comma separated list of IP addresses or network addresses to allow access. Add ! character at the start to deny access, e.g. !192.168.10.0/24 will deny entire subnet. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all except loopback. Valid only for `UseSpecifiedNetworkACL` recursion option. | | DNS_SERVER_RECURSION_DENIED_NETWORKS | String | A comma separated list of IP addresses or network addresses to deny recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. | | DNS_SERVER_RECURSION_ALLOWED_NETWORKS | String | A comma separated list of IP addresses or network addresses to allow recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. | | DNS_SERVER_ENABLE_BLOCKING | Boolean | Sets the DNS server to block domain names using Blocked Zone and Block List Zone. | | DNS_SERVER_ALLOW_TXT_BLOCKING_REPORT | Boolean | Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. | | DNS_SERVER_BLOCK_LIST_URLS | String | A comma separated list of block list URLs. | | DNS_SERVER_FORWARDERS | String | A comma separated list of forwarder addresses. | | DNS_SERVER_FORWARDER_PROTOCOL | String | Forwarder protocol options: `Udp`, `Tcp`, `Tls`, `Https`, `HttpsJson`. | | DNS_SERVER_LOG_USING_LOCAL_TIME | Boolean | Enable this option to use local time instead of UTC for logging. | ================================================ FILE: Dockerfile ================================================ # syntax=docker.io/docker/dockerfile:1 FROM mcr.microsoft.com/dotnet/aspnet:9.0 # Add the MS repo to install `libmsquic` to support DNS-over-QUIC: ADD --link https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb / RUN < HTTP/3) (TCP => HTTP/1.1 + HTTP/2) 443/udp 443/tcp \ # DNS-over-HTTP (for when running behind a reverse-proxy that terminates TLS) 80/tcp 8053/tcp \ # Technitium web console + API (HTTP / HTTPS) 5380/tcp 53443/tcp \ # DHCP 67/udp # https://specs.opencontainers.org/image-spec/annotations/ # https://github.com/opencontainers/image-spec/blob/main/annotations.md LABEL org.opencontainers.image.title="Technitium DNS Server" LABEL org.opencontainers.image.vendor="Technitium" LABEL org.opencontainers.image.source="https://github.com/TechnitiumSoftware/DnsServer" LABEL org.opencontainers.image.url="https://technitium.com/dns/" LABEL org.opencontainers.image.authors="support@technitium.com" ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================

    Technitium DNS Server
    Technitium DNS Server


    Self host a DNS server for privacy & security
    Block ads & malware at DNS level for your entire network!

    Technitium DNS Server

    Technitium DNS Server is an open source authoritative as well as recursive DNS server that can be used for self hosting a DNS server for privacy & security. It works out-of-the-box with no or minimal configuration and provides a user friendly web console accessible using any modern web browser. Nobody really bothers about domain name resolution since it works automatically behind the scenes and is complex to understand. Most computer software use the operating system's DNS resolver that usually query the configured ISP's DNS server using UDP protocol. This way works well for most people but, your ISP can see and control what website you can visit even when the website employ HTTPS security. Not only that, some ISPs can redirect, block or inject content into websites you visit even when you use a different DNS provider like Google DNS or Cloudflare DNS. Having Technitium DNS Server configured to use [DNS-over-TLS](https://en.wikipedia.org/wiki/DNS_over_TLS), [DNS-over-HTTPS](https://en.wikipedia.org/wiki/DNS_over_HTTPS), or [DNS-over-QUIC](https://www.ietf.org/rfc/rfc9250.html) forwarders, these privacy & security issues can be mitigated very effectively. Be it a home network or an organization's network, having a locally running DNS server gives you more insights into your network and helps to understand it better using the DNS logs and stats. It improves overall performance since most queries are served from the DNS cache making web sites load faster by not having to wait for frequent DNS resolutions. It also gives you an additional control over your network allowing you to block domain names network wide and also allows you to route your DNS traffic securely using encrypted DNS protocols. # Sponsored By

    Altha Technology - Censorship Resistant Data Services

    Bartell Hotels - San Diego's Unforgettable Locations Technology Investors and Integrators | WavSpeed Inc | Texas

    # Features - Works on Windows, Linux, macOS and Raspberry Pi. - Docker image available on [Docker Hub](https://hub.docker.com/r/technitium/dns-server). - Installs in just a minute and works out-of-the-box with zero configuration. - Block ads & malware using one or more block list URLs. - Supports working as an authoritative as well as a recursive DNS server. - Includes built-in Clustering feature to allow managing two or more DNS server instances from a single admin web console. - High performance DNS server based on async IO that can serve millions of requests per minute even on a commodity desktop PC hardware (load tested on Intel i7-8700 CPU with more than 100,000 request/second over Gigabit Ethernet). - Self host [DNS-over-TLS](https://www.rfc-editor.org/rfc/rfc7858.html), [DNS-over-HTTPS](https://www.rfc-editor.org/rfc/rfc8484.html), and [DNS-over-QUIC](https://www.ietf.org/rfc/rfc9250.html) DNS services on your network. - DNS-over-HTTPS implementation supports HTTP/1.1, HTTP/2, and HTTP/3 transport protocols. - Supports DNS over [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) version 1 and 2 for both UDP and TCP transports. - Use public DNS resolvers like Cloudflare, Google, Quad9, and AdGuard with [DNS-over-TLS](https://www.rfc-editor.org/rfc/rfc7858.html), [DNS-over-HTTPS](https://www.rfc-editor.org/rfc/rfc8484.html), or [DNS-over-QUIC](https://www.ietf.org/rfc/rfc9250.html) protocols as forwarders. - Support for latency based name server selection algorithm that works with concurrency feature for both recursive resolution and forwarders. - Advanced caching with features like serve stale, prefetching and auto prefetching. - Persistent caching feature that saves cache to disk when DNS server restarts. - DNS rebinding attack protection feature available with DNS Rebinding Protection App. - DNSSEC validation support with RSA, ECDSA & EdDSA algorithms for recursive resolver, forwarders, and conditional forwarders with NSEC and NSEC3 support. - DNSSEC support for all supported DNS transport protocols including encrypted DNS protocols. - DANE TLSA [RFC 6698](https://datatracker.ietf.org/doc/html/rfc6698) record type support. This includes support for automatically generating the hash values using certificates in PEM format. - SVCB & HTTPS [draft-ietf-dnsop-svcb-https](https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-12.html) record type support. - URI [RFC 7553](https://www.rfc-editor.org/rfc/rfc7553.html) record type support. - SSHFP [RFC 4255](https://www.rfc-editor.org/rfc/rfc4255.html) record type support. - CNAME cloaking feature to block domain names that resolve to CNAME which are blocked. - QNAME minimization support in recursive resolver [RFC 9156](https://www.rfc-editor.org/rfc/rfc9156.html). - QNAME case randomization support for UDP transport protocol [draft-vixie-dnsext-dns0x20-00](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00). - DNAME record [RFC 6672](https://datatracker.ietf.org/doc/html/rfc6672) support. - ANAME proprietary record support to allow using CNAME like feature at zone apex (CNAME flattening). Supports multiple ANAME records at both zone apex and sub domains. - APP proprietary record support that allows custom DNS Apps to directly handle DNS requests and return a custom DNS response based on any business logic. - Support for features like Split Horizon and Geolocation based responses using DNS Apps feature. - Support for REGEX based block lists with different block lists for different client IP addresses or subnet using Advanced Blocking DNS App. - Primary, Secondary, Stub, and Conditional Forwarder zone support. - Static stub zone support implemented in Conditional Forwarder zone to force a domain name to resolve via given name servers using NS records. - Supports Catalog Zones [RFC 9432](https://datatracker.ietf.org/doc/rfc9432/). - Supports record aging where the records with expiry set are automatically removed from the zone. - Bulk conditional forwarding support using Advanced Forwarding DNS App. - DNSSEC signed zones support with RSA, ECDSA & EdDSA algorithms. - DNSSEC support for both NSEC and NSEC3. - Zone transfer with AXFR and IXFR [RFC 1995](https://www.rfc-editor.org/rfc/rfc1995.html) and DNS NOTIFY [RFC 1996](https://www.rfc-editor.org/rfc/rfc1996.html) support. - Zone transfer over TLS (XFR-over-TLS) [RFC 9103](https://www.rfc-editor.org/rfc/rfc9103.html) support. - Zone transfer over QUIC (XFR-over-QUIC) [RFC 9250](https://www.ietf.org/rfc/rfc9250.html) support. - Support for zone validation using ZONEMD records [RFC 8976](https://datatracker.ietf.org/doc/rfc8976/) for Secondary zones. - Dynamic DNS Updates [RFC 2136](https://www.rfc-editor.org/rfc/rfc2136) support with security policy. - Secret key transaction authentication (TSIG) [RFC 8945](https://datatracker.ietf.org/doc/html/rfc8945) support for zone transfers. - EDNS(0) [RFC6891](https://datatracker.ietf.org/doc/html/rfc6891) support. - EDNS Client Subnet (ECS) [RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871) support for recursive resolution and forwarding. - Extended DNS Errors [RFC 8914](https://datatracker.ietf.org/doc/html/rfc8914) support. - DNS64 function [RFC 6147](https://www.rfc-editor.org/rfc/rfc6147) support for use by IPv6 only clients using the DNS64 App. - Support to host DNSBL / RBL block lists [RFC 5782](https://www.rfc-editor.org/rfc/rfc5782). - Multi-user role based access with non-expiring API token support. - Self host your domain names on your own DNS server. - Wildcard sub domain support. - Enable/disable zones and records to allow testing with ease. - Built-in DNS Client with option to import responses to local zone. - Supports out-of-order DNS request processing for DNS-over-TCP and DNS-over-TLS protocols [RFC 7766](https://www.rfc-editor.org/rfc/rfc7766#section-7). - Built-in DHCP Server that can work for multiple networks. - IPv6 support in DNS server core. - HTTP & SOCKS5 proxy support which can be configured to route DNS over [Tor Network](https://www.torproject.org/) or use [Cloudflare's hidden DNS resolver](https://blog.cloudflare.com/welcome-hidden-resolver/). - Admin web console for easy configuration using any web browser with support for Dark Mode. - Built in HTTP API to allow 3rd party apps to control and configure the DNS server. - Supports TOTP based Two-factor authentication (2FA). - Built-in system logging and query logging. - Open source cross-platform .NET 9 implementation hosted on [GitHub](https://github.com/TechnitiumSoftware/DnsServer). # Installation - **Windows**: [Download setup installer](https://download.technitium.com/dns/DnsServerSetup.zip) for easy installation. - **Linux & Raspberry Pi**: Follow install instructions from [this blog post](https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html). - **Cross-Platform**: [Download portable app](https://download.technitium.com/dns/DnsServerPortable.tar.gz) to run on any platform that has .NET 9 installed. - **Docker**: Pull the official image from [Docker Hub](https://hub.docker.com/r/technitium/dns-server). Use the [docker-compose.yml](https://github.com/TechnitiumSoftware/DnsServer/blob/master/docker-compose.yml) example to create a new container and edit it as required for your deployments. For more details and troubleshooting read the [install instructions](https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html). # Build Instructions You can build the DNS server from source and install it manually by following the [Build Instructions](https://github.com/TechnitiumSoftware/DnsServer/blob/master/build.md). # Docker Environment Variables Technitium DNS Server supports environment variables to allow initializing the config when the DNS server starts for the first time. Read the [environment variable documentation](https://github.com/TechnitiumSoftware/DnsServer/blob/master/DockerEnvironmentVariables.md) for complete details. # API Documentation The DNS server HTTP API allows any 3rd party app or script to configure the DNS server. The HTTP API is used by the web console and thus all the actions that the web console does can be performed via the API. Read the [HTTP API documentation](https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md) for complete details. # Help Topics Read the latest [online help topics](https://go.technitium.com/?id=25) which contains the DNS Server user manual and covers frequently asked questions. # Support For support, send an email to support@technitium.com. For any issues, feedback, or feature request, create an issue on [GitHub](https://github.com/TechnitiumSoftware/DnsServer/issues). Join [/r/technitium](https://www.reddit.com/r/technitium/) on Reddit. # Donate Make contribution to Technitium and help making new software, updates, and features possible. [Donate Now!](https://www.patreon.com/technitium) # Blog Posts - [Technitium Blog: Understanding Clustering And How To Configure It](https://blog.technitium.com/2025/11/understanding-clustering-and-how-to.html) (Nov 2025) - [How-To Geek: These open-source DNS tools block annoyances and speed up your browsing](https://www.howtogeek.com/block-ad-traffic-and-speed-up-your-browsing-with-these-3-free-open-source-dns-tools/) (Nov 2025) - [Technitium Blog: Technitium DNS Server v14 Released!](https://blog.technitium.com/2025/11/technitium-dns-server-v14-released.html) (Nov 2025) - [XDA: Technitium is the best local DNS tool you can deploy](https://www.xda-developers.com/technitium-best-local-dns-tool/) (Aug 2025) - [Technitium Blog: How To Configure Catalog Zones For Automatic Provisioning Of Secondary Zones](https://blog.technitium.com/2024/10/how-to-configure-catalog-zones-for.html) (Oct 2024) - [Technitium Blog: Technitium DNS Server v13 Released!](https://blog.technitium.com/2024/09/technitium-dns-server-v13-released.html) (Sept 2024) - [Technitium Blog: Technitium DNS Server v12 Released!](https://blog.technitium.com/2024/02/technitium-dns-server-v12-released.html) (Feb 2024) - [Technitium Blog: For DNSSEC And Why DANE Is Needed](https://blog.technitium.com/2023/05/for-dnssec-and-why-dane-is-needed.html) (May 2023) - [Technitium Blog: How To Auto Renew SSL Certificates With Certbot Using DNS Challenge](https://blog.technitium.com/2023/03/how-to-auto-renew-ssl-certificates-with.html) (Mar 2023) - [Technitium Blog: Configuring DNS-over-QUIC and HTTPS/3 For Technitium DNS Server](https://blog.technitium.com/2023/02/configuring-dns-over-quic-and-https3.html) (Feb 2023) - [Technitium Blog: Technitium DNS Server v11 Released!](https://blog.technitium.com/2023/02/technitium-dns-server-v11-released.html) (Feb 2023) - [Technitium Blog: Technitium DNS Server v10 Released!](https://blog.technitium.com/2022/11/technitium-dns-server-v10-released.html) (Nov 2022) - [Technitium Blog: Technitium DNS Server v9 Released!](https://blog.technitium.com/2022/09/technitium-dns-server-v9-released.html) (Sept 2022) - [Technitium Blog: How To Secure Your Domain Name With DNSSEC](https://blog.technitium.com/2022/07/how-to-secure-your-domain-name-with-.html) (Jul 2022) - [Technitium Blog: How To Self Host Your Own Domain Name](https://blog.technitium.com/2022/06/how-to-self-host-your-own-domain-name.html) (Jun 2022) - [Technitium Blog: Technitium DNS Server v8 Released!](https://blog.technitium.com/2022/03/technitium-dns-server-v8-released.html) (Mar 2022) - [Technitium Blog: Running A Root Server Locally On Your DNS Resolver](https://blog.technitium.com/2021/07/running-root-server-locally-on-your-dns.html) (Jul 2021) - [Yolan Romailler: Being ad-free on Android without rooting](https://romailler.ch/2021/04/15/misc-pihole_over_dot/) (Apr 2021) - [Technitium Blog: Creating And Running DNS Apps On Technitium DNS Server](https://blog.technitium.com/2021/03/creating-and-running-dns-apps-on.html) (Mar 2021) - [Technitium Blog: How To Host Your Own DNS-over-HTTPS And DNS-over-TLS Services](https://blog.technitium.com/2020/07/how-to-host-your-own-dns-over-https-and.html) (Oct 2020) - [Technitium Blog: How To Disable Firefox DNS-over-HTTPS On Your Network](https://blog.technitium.com/2020/07/how-to-disable-firefox-dns-over-https.html) (Jul 2020) - [Technitium Blog: How To Enforce Google Safe Search And YouTube Restricted Mode On Your Network](https://blog.technitium.com/2020/07/how-to-enforce-google-safe-search-and.html) (Jul 2020) - [Technitium Blog: Technitium DNS Server v5 Released!](https://blog.technitium.com/2020/07/technitium-dns-server-v5-released.html) (Jul 2020) - [Brian Wojtczak: Keep It Encrypted, Keep It Safe: Working with ESNI, DoH, and DoT](https://www.toptal.com/web/encrypted-safe-with-esni-doh-dot) (Jan 2020) - [phra's blog: Exfiltrate Like a Pro: Using DNS over HTTPS as a C2 Channel](https://iwantmore.pizza/posts/dnscat2-over-doh.html) (Aug 2019) - [Scott Hanselman: Exploring DNS with the .NET Core based Technitium DNS Server](https://www.hanselman.com/blog/ExploringDNSWithTheNETCoreBasedTechnitiumDNSServer.aspx) (Apr 2019) - [Technitium Blog: Turn Raspberry Pi Into Network Wide DNS Server](https://blog.technitium.com/2019/01/turn-raspberry-pi-into-network-wide-dns.html) (Jan 2019) - [Technitium Blog: Blocking Internet Ads Using DNS Sinkhole](https://blog.technitium.com/2018/10/blocking-internet-ads-using-dns-sinkhole.html) (Oct 2018) - [Technitium Blog: Configuring DNS Server For Privacy & Security](https://blog.technitium.com/2018/06/configuring-dns-server-for-privacy.html) (Jun 2018) - [Technitium Blog: Technitium DNS Server v1.3 Released!](https://blog.technitium.com/2018/06/technitium-dns-server-v13-released.html) (Jun 2018) - [Technitium Blog: Running Technitium DNS Server on Ubuntu Linux](https://blog.technitium.com/2017/11/running-dns-server-on-ubuntu-linux.html) (Nov 2017) - [Technitium Blog: Technitium DNS Server Released!](https://blog.technitium.com/2017/11/technitium-dns-server-released.html) (Nov 2017) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Only the latest available version of Technitium DNS Server is supported for security updates. ## Reporting a Vulnerability To report a vulnerability send an email to security@technitium.com ================================================ FILE: build.md ================================================ # Build Instructions ## For Windows To build the Technitium DNS Server Windows Setup, you need to install [Microsoft Visual Studio Community 2022 (VS2022)](https://visualstudio.microsoft.com/vs/) and [Inno Setup](https://jrsoftware.org/isinfo.php) on your computer. Once you have it installed, follow the steps below: 1. Open VS2022 and use the "Clone a repository" option to clone the [TechnitiumLibrary](https://github.com/TechnitiumSoftware/TechnitiumLibrary) project using the `https://github.com/TechnitiumSoftware/TechnitiumLibrary.git` URL. Once the repository is cloned and opened in VS2022, select the build mode to "Release" from the dropdown box in the toolbar and use the Build > Build Solution menu to build it. 2. Open VS2022 and use the "Clone a repository" option to clone the [DnsServer](https://github.com/TechnitiumSoftware/DnsServer) project using the `https://github.com/TechnitiumSoftware/DnsServer.git` URL in the same parent folder that you had cloned the TechnitiumLibrary repository in previous step. Once the repository is cloned and opened in VS2022, right click on the `DnsServerSystemTrayApp` project and click on the Publish menu to open the publish page. Click the Publish button on it to publish the project in `DnsServer\DnsServerWindowsSetup\publish` folder. Similarly, right click on the `DnsServerWindowsService` project and click on the Publish menu to open publish page and use the Publish button to publish the project in the same folder as that of the previous project. 3. Open the `DnsServer\DnsServerWindowsSetup\DnsServerSetup.iss` file in Inno Setup and click on the Build > Compile menu to generate a Windows setup in `DnsServerWindowsSetup\Release` folder that you can then use to install Technitium DNS Server on Windows. ## For Linux Follow the instructions given below to build and install the DNS server from source. These instructions are written for Ubuntu and Raspberry Pi OS but, you can easily follow similar steps on your favorite distro. 1. Install prerequisites like curl and git. ``` sudo apt update sudo apt install curl git -y ``` 2. Follow the [install instructions](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-install?tabs=dotnet9&pivots=os-linux-ubuntu-2404) to be able to install ASP.NET Core SDK on your distro. Use the instructions given in the link to install the repository for other distros not shown in below examples: - Ubuntu 24.04 ``` sudo add-apt-repository ppa:dotnet/backports sudo apt update ``` - Raspberry Pi OS ``` curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - sudo apt-add-repository https://packages.microsoft.com/debian/11/prod sudo apt update ``` 3. Install ASP.NET Core 9 SDK and `libmsquic` for DNS-over-QUIC support. ``` sudo apt install dotnet-sdk-9.0 libmsquic -y ``` Note! If you do not plan to use DNS-over-QUIC or HTTP/3 support, or you intend to just build a docker image then you can skip installing `libmsquic`. 4. Clone the source code for both [TechnitiumLibrary](https://github.com/TechnitiumSoftware/TechnitiumLibrary) and [DnsServer](https://github.com/TechnitiumSoftware/DnsServer) into the current folder. ``` git clone --depth 1 https://github.com/TechnitiumSoftware/TechnitiumLibrary.git TechnitiumLibrary git clone --depth 1 https://github.com/TechnitiumSoftware/DnsServer.git DnsServer ``` 5. Build the TechnitiumLibrary source. ``` dotnet build TechnitiumLibrary/TechnitiumLibrary.ByteTree/TechnitiumLibrary.ByteTree.csproj -c Release dotnet build TechnitiumLibrary/TechnitiumLibrary.Net/TechnitiumLibrary.Net.csproj -c Release dotnet build TechnitiumLibrary/TechnitiumLibrary.Security.OTP/TechnitiumLibrary.Security.OTP.csproj -c Release ``` 6. Build the DnsServer source. ``` dotnet publish DnsServer/DnsServerApp/DnsServerApp.csproj -c Release ``` 7. Install the DNS server as a systemd service. Note! Skip this step if you wish to build and use docker image. ``` sudo mkdir -p /opt/technitium/dns sudo cp -r DnsServer/DnsServerApp/bin/Release/publish/* /opt/technitium/dns sudo cp /opt/technitium/dns/systemd.service /etc/systemd/system/dns.service sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved sudo systemctl enable dns.service sudo systemctl start dns.service sudo rm /etc/resolv.conf echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf ``` 8. Build and run docker image. Note! Skip this step if you have already installed the DNS server as a systemd service in previous step. Note! Before proceeding to build a Docker image, it is required that you have installed `docker` on your computer. Follow the commands given below to build a docker image for the DNS server. ``` cd DnsServer sudo docker build -t technitium/dns-server:latest . ``` You can now run the image that you have built using `docker compose` as shown below. You should edit the `docker-compose.yml` file if you wish to edit the container's configuration before running it. ``` sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved sudo docker compose up -d ``` 9. Open the DNS server web console in a web browser using `http://:5380/` URL and set a login password to complete the installation. ================================================ FILE: docker-compose.yml ================================================ services: dns-server: container_name: dns-server hostname: dns-server image: technitium/dns-server:latest # For DHCP deployments, use "host" network mode and remove all the port mappings, including the ports array by commenting them # network_mode: "host" ports: - "5380:5380/tcp" #DNS web console (HTTP) # - "53443:53443/tcp" #DNS web console (HTTPS) - "53:53/udp" #DNS service - "53:53/tcp" #DNS service # - "853:853/udp" #DNS-over-QUIC service # - "853:853/tcp" #DNS-over-TLS service # - "443:443/udp" #DNS-over-HTTPS service (HTTP/3) # - "443:443/tcp" #DNS-over-HTTPS service (HTTP/1.1, HTTP/2) # - "80:80/tcp" #DNS-over-HTTP service (use with reverse proxy or certbot certificate renewal) # - "8053:8053/tcp" #DNS-over-HTTP service (use with reverse proxy) # - "67:67/udp" #DHCP service environment: - DNS_SERVER_DOMAIN=dns-server #The primary domain name used by this DNS Server to identify itself. # - DNS_SERVER_ADMIN_PASSWORD=password #DNS web console admin user password. # - DNS_SERVER_ADMIN_PASSWORD_FILE=password.txt #The path to a file that contains a plain text password for the DNS web console admin user. # - DNS_SERVER_PREFER_IPV6=false #DNS Server will use IPv6 for querying whenever possible with this option enabled. # - DNS_SERVER_WEB_SERVICE_LOCAL_ADDRESSES=172.17.0.1,127.0.0.1 #Comma separated list of network interface IP addresses that you want the web service to listen on for requests. The "172.17.0.1" address is the built-in Docker bridge. The "[::]" is the default value if not specified. Note! This must be used only with "host" network mode. # - DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380 #The TCP port number for the DNS web console over HTTP protocol. # - DNS_SERVER_WEB_SERVICE_HTTPS_PORT=53443 #The TCP port number for the DNS web console over HTTPS protocol. # - DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS=false #Enables HTTPS for the DNS web console. # - DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT=false #Enables self signed TLS certificate for the DNS web console. # - DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PATH=/etc/dns/tls/cert.pfx #The file path to the TLS certificate for the DNS web console. # - DNS_SERVER_WEB_SERVICE_TLS_CERTIFICATE_PASSWORD=password #The password for the TLS certificate for the DNS web console. # - DNS_SERVER_WEB_SERVICE_HTTP_TO_TLS_REDIRECT=false #Enables HTTP to HTTPS redirection for the DNS web console. # - DNS_SERVER_OPTIONAL_PROTOCOL_DNS_OVER_HTTP=false #Enables DNS server optional protocol DNS-over-HTTP on TCP port 8053 to be used with a TLS terminating reverse proxy like nginx. # - DNS_SERVER_RECURSION=AllowOnlyForPrivateNetworks #Recursion options: Allow, Deny, AllowOnlyForPrivateNetworks, UseSpecifiedNetworkACL. # - DNS_SERVER_RECURSION_NETWORK_ACL=192.168.10.0/24, !192.168.10.2 #Comma separated list of IP addresses or network addresses to allow access. Add ! character at the start to deny access, e.g. !192.168.10.0/24 will deny entire subnet. The ACL is processed in the same order its listed. If no networks match, the default policy is to deny all except loopback. Valid only for `UseSpecifiedNetworkACL` recursion option. # - DNS_SERVER_RECURSION_DENIED_NETWORKS=1.1.1.0/24 #Comma separated list of IP addresses or network addresses to deny recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. # - DNS_SERVER_RECURSION_ALLOWED_NETWORKS=127.0.0.1, 192.168.1.0/24 #Comma separated list of IP addresses or network addresses to allow recursion. Valid only for `UseSpecifiedNetworkACL` recursion option. This option is obsolete and DNS_SERVER_RECURSION_NETWORK_ACL should be used instead. # - DNS_SERVER_ENABLE_BLOCKING=false #Sets the DNS server to block domain names using Blocked Zone and Block List Zone. # - DNS_SERVER_ALLOW_TXT_BLOCKING_REPORT=false #Specifies if the DNS Server should respond with TXT records containing a blocked domain report for TXT type requests. # - DNS_SERVER_BLOCK_LIST_URLS= #A comma separated list of block list URLs. # - DNS_SERVER_FORWARDERS=1.1.1.1, 8.8.8.8 #Comma separated list of forwarder addresses. # - DNS_SERVER_FORWARDER_PROTOCOL=Tcp #Forwarder protocol options: Udp, Tcp, Tls, Https, HttpsJson. # - DNS_SERVER_LOG_USING_LOCAL_TIME=true #Enable this option to use local time instead of UTC for logging. volumes: - config:/etc/dns restart: unless-stopped sysctls: - net.ipv4.ip_local_port_range=1024 65535 volumes: config: